Miniproyecto Sección 10#
Importación de Librerías#
Se importan las librerías esenciales para facilitar el análisis, que abarca la carga de datos, la visualización, la transformación, la fusión y la unión. Además, se configura el entorno para suprimir las advertencias (warnings) y asegurar que la salida del código sea más limpia y enfocada en los resultados.
import warnings
warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
import lime
import lime.lime_tabular
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from scipy.stats.mstats import winsorize
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, KBinsDiscretizer
from sklearn.naive_bayes import GaussianNB, BernoulliNB, MultinomialNB
from sklearn.linear_model import LogisticRegressionCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import Binarizer
from sklearn.model_selection import cross_val_score
from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_curve, auc
Lectura del Dataset#
Para iniciar el proceso de análisis, se procede a cargar el dataset descargado en el entorno de trabajo utilizando la biblioteca Pandas. El archivo heart.csv contiene toda la información clínica de los pacientes que será objeto de estudio en este proyecto.
df = pd.read_csv("C:/Users/david/Downloads/heart.csv")
Se muestran las primeras filas del DataFrame.
df.head()
| Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 40 | M | ATA | 140 | 289 | 0 | Normal | 172 | N | 0.0 | Up | 0 |
| 1 | 49 | F | NAP | 160 | 180 | 0 | Normal | 156 | N | 1.0 | Flat | 1 |
| 2 | 37 | M | ATA | 130 | 283 | 0 | ST | 98 | N | 0.0 | Up | 0 |
| 3 | 48 | F | ASY | 138 | 214 | 0 | Normal | 108 | Y | 1.5 | Flat | 1 |
| 4 | 54 | M | NAP | 150 | 195 | 0 | Normal | 122 | N | 0.0 | Up | 0 |
Se muestran las últimas filas del DataFrame.
df.tail()
| Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 913 | 45 | M | TA | 110 | 264 | 0 | Normal | 132 | N | 1.2 | Flat | 1 |
| 914 | 68 | M | ASY | 144 | 193 | 1 | Normal | 141 | N | 3.4 | Flat | 1 |
| 915 | 57 | M | ASY | 130 | 131 | 0 | Normal | 115 | Y | 1.2 | Flat | 1 |
| 916 | 57 | F | ATA | 130 | 236 | 0 | LVH | 174 | N | 0.0 | Flat | 1 |
| 917 | 38 | M | NAP | 138 | 175 | 0 | Normal | 173 | N | 0.0 | Up | 0 |
Se emplea el método df.info() para evaluar la estructura y calidad inicial del dataset. Este análisis revela información sobre la composición de los datos, incluyendo el número total de registros (filas) y variables (columnas), los tipos de datos asociados a cada campo (como objetos, enteros o valores flotantes), y la cantidad de valores no nulos por columna.
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 918 entries, 0 to 917
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Age 918 non-null int64
1 Sex 918 non-null object
2 ChestPainType 918 non-null object
3 RestingBP 918 non-null int64
4 Cholesterol 918 non-null int64
5 FastingBS 918 non-null int64
6 RestingECG 918 non-null object
7 MaxHR 918 non-null int64
8 ExerciseAngina 918 non-null object
9 Oldpeak 918 non-null float64
10 ST_Slope 918 non-null object
11 HeartDisease 918 non-null int64
dtypes: float64(1), int64(6), object(5)
memory usage: 86.2+ KB
El resultado del método info() indica que el dataset está en un estado óptimo y listo para el análisis, sin problemas inmediatos de integridad de datos. A continuación, se desglosa el análisis:
1. Dimensión del Dataset:
RangeIndex: 918 entries, 0 to 917: El dataset contiene 918 registros o pacientes, cada uno identificado por un índice del 0 al 917. Este es un tamaño muy adecuado para un proyecto de análisis y machine learning.
2. Integridad de los Datos (Ausencia de Valores Nulos):
Non-Null Count: 918 non-nullpara todas las columnas. Este es el hallazgo más importante. No hay valores faltantes (Missing Values) en ninguna de las 12 columnas. Esto elimina la necesidad de realizar técnicas de imputación o eliminación de datos, simplificando significativamente la fase de preprocesamiento.
3. Tipos de Datos (Dtypes): Los tipos de datos están correctamente asignados, lo que sugiere que la carga del archivo CSV fue exitosa. Se identifican tres tipos:
int64(6 columnas): Variables enteras. IncluyenAge(edad),RestingBP(presión arterial),Cholesterol(colesterol),FastingBS(glucemia, que es binaria pero codificada como 0/1),MaxHR(frecuencia cardíaca máxima) y la variable objetivoHeartDisease. Son correctos.float64(1 columna): Variable decimal. SoloOldpeak(depresión del ST) tiene este tipo, lo cual es apropiado ya que esta medida clínica suele ser un valor continuo.object(5 columnas): Variables categóricas (texto). IncluyenSex,ChestPainType,RestingECG,ExerciseAnginayST_Slope. Antes de modelar, será necesario convertirlas a formato numérico mediante técnicas como One-Hot Encoding.
4. Uso de Memoria:
memory usage: 86.2+ KB: El dataset es muy liviano, ocupando aproximadamente 86 kilobytes en memoria. Esto permite manipularlo y procesarlo con gran facilidad sin preocupaciones por rendimiento.
Valores Únicos#
Se calcula el número de valores únicos para cada variable del DataFrame.
df.nunique()
Age 50
Sex 2
ChestPainType 4
RestingBP 67
Cholesterol 222
FastingBS 2
RestingECG 3
MaxHR 119
ExerciseAngina 2
Oldpeak 53
ST_Slope 3
HeartDisease 2
dtype: int64
Este análisis complementa la información de df.info() y revela el grado de diversidad y el tipo de variable con la que trabajamos en cada columna.
Variables Categóricas:
Sex: 2: Confirma que es una variable binaria con dos categorías, ‘M’ y ‘F’.ChestPainType: 4: Existen los 4 tipos de dolor torácico definidos en la descripción: TA, ATA, NAP, ASY.RestingECG: 3: Coincide con las 3 categorías esperadas: Normal, ST, LVH.ExerciseAngina: 2: Variable binaria con dos valores: ‘Y’ (Yes) y ‘N’ (No).ST_Slope: 3: Confirma las 3 pendientes posibles: Up, Flat, Down.FastingBS: 2&HeartDisease: 2: Ambas son variables binarias codificadas numéricamente (0 y 1), lo cual es correcto.
Variables Numéricas:
Age: 50: Hay 50 valores únicos de edad en 918 pacientes. Esto sugiere una distribución bastante diversa y que la edad está representada en un rango amplio, lo cual es positivo para el modelo.RestingBP: 67: Existen 67 valores únicos de presión arterial. Esto indica que es una variable continua con buena variabilidad, pero se debe verificar que no haya valores erróneos (ej., presiones arteriales de 0 mm Hg, que serían inviables).Cholesterol: 222: Este es un hallazgo crucial y que requiere atención inmediata. Tener 222 valores únicos en 918 registros sugiere que es una variable continua con una variabilidad muy alta en la muestra.MaxHR: 119: Tiene 119 valores únicos, lo que es esperable para una medida de frecuencia cardíaca máxima, comportándose como una variable continua saludable.Oldpeak: 53: Presenta 53 valores únicos, lo que confirma su naturaleza de variable numérica continua (float) con una precisión decimal adecuada.
Análisis Univariado#
El Análisis Univariado es la primera fase del análisis exploratorio de datos. Se enfoca en el estudio individual de cada variable para entender su distribución, características y valores atípicos. Esto permite identificar patrones y la calidad de los datos antes de un análisis más complejo.
Variables Binarias#
Se identifican y analizan las variables binarias (aquellas con solo 2 valores únicos) en el DataFrame. Luego, se genera un doble gráfico para cada variable binaria, permitiendo comparar su distribución mediante:
Gráfico de Barras:
Muestra el conteo absoluto de cada categoría.
Ideal para comparar magnitudes visualmente.
Gráfico de Torta:
Muestra la proporción porcentual de cada categoría.
Útil para entender el balance/imbalance entre clases.
plt.style.use('seaborn-v0_8-darkgrid')
binarias = [col for col in df.columns if df[col].nunique() == 2]
for col in binarias:
print(f"\n=== {col.upper()} ===")
plt.figure(figsize=(10, 5))
value_order = sorted(df[col].unique())
colors = sns.color_palette('Set2')
plt.subplot(1, 2, 1)
ax = sns.countplot(x=df[col], palette=colors, order=value_order)
plt.title(f'Distribución de {col}')
for container in ax.containers:
ax.bar_label(container, fmt='%d')
plt.subplot(1, 2, 2)
counts = df[col].value_counts().loc[value_order]
plt.pie(counts,
labels=counts.index,
autopct='%1.1f%%',
colors=colors)
plt.title(f'Distribución de {col}')
plt.tight_layout()
plt.show()
=== SEX ===
=== FASTINGBS ===
=== EXERCISEANGINA ===
=== HEARTDISEASE ===
1. Distribución de Sex (Sexo)
Proporción: 79% Masculino (M) vs 21% Femenino (F).
Interpretación: Existe un desequilibrio significativo en la representación por sexo. El dataset está predominantemente compuesto por pacientes masculinos (725 hombres vs 193 mujeres).
2. Distribución de FastingBS (Glucemia en Ayunas)
Proporción: 76.7% con valor 0 (FastingBS ≤ 120 mg/dl) vs 23.3% con valor 1 (FastingBS > 120 mg/dl).
Interpretación: La mayoría de los pacientes presentan niveles normales de azúcar en sangre en ayunas. Aproximadamente 1 de cada 4 pacientes (214 individuos) tiene glucemia elevada, que es un factor de riesgo conocido para enfermedades cardiovasculares.
3. Distribución de ExerciseAngina (Angina Inducida por Ejercicio)
Proporción: 59.6% No (N) vs 40.4% Sí (Y).
Interpretación: La angina inducida por el ejercicio está presente en una proporción muy alta de pacientes (371 casos). Dado que esta es una variable clínica directamente relacionada con la isquemia cardíaca, se espera que sea un predictor muy fuerte de la enfermedad.
4. Distribución de HeartDisease (Variable Objetivo - Enfermedad Cardíaca)
Proporción: 55.3% con enfermedad (1) vs 44.7% sin enfermedad (0).
Interpretación: El dataset está ligeramente desbalanceado a favor de la clase positiva (enfermedad presente). Hay 508 pacientes con enfermedad cardíaca y 410 sin ella.
Variables Categóricas#
Se generan gráficos de barras para visualizar la distribución de cada variable categórica.
Cada gráfico muestra:
El número absoluto de observaciones (conteo).
El porcentaje relativo respecto al total.
Las categorías ordenadas de mayor a menor.
categoricas = df.columns[df.dtypes == 'object'].tolist()
categoricas = [col for col in categoricas if df[col].nunique() > 2]
for col in categoricas:
print(f"\n=== {col.upper()} ===")
plt.figure(figsize=(10, 5))
value_order = df[col].value_counts().index
ax = sns.countplot(x=df[col], order=value_order, palette='Set2')
for p in ax.patches:
count = int(p.get_height())
percent = 100 * p.get_height() / len(df[col])
ax.annotate(f'{count}\n({percent:.1f}%)',
(p.get_x() + p.get_width()/2., p.get_height()),
ha='center', va='center',
xytext=(0, -14),
textcoords='offset points')
plt.title(f'Distribución de {col}')
plt.tight_layout()
plt.show()
=== CHESTPAINTYPE ===
=== RESTINGECG ===
=== ST_SLOPE ===
1. Distribución de ChestPainType (Tipo de Dolor Torácico)
Proporción: ASY (Asintomático): 54.0% > NAP (Dolor No Anginoso): 22.1% > ATA (Angina Atípica): 18.8% > TA (Angina Típica): 5.0%
Interpretación: Existe una predominancia abrumadora de pacientes asintomáticos (ASY). Más de la mitad de la cohorte no reportó dolor torácico, a pesar de estar siendo evaluados por enfermedad cardíaca.
Implicación para el modelo: Este es un hallazgo clínicamente crucial. La ausencia de síntomas (ASY) es, en este contexto, un potente predictor de enfermedad cardíaca. Sugiere que la condición puede estar enmascarada o ser “silenciosa”, haciendo que las otras variables clínicas y de pruebas de esfuerzo sean aún más importantes para el diagnóstico. La angina típica (TA), clásicamente asociada a problemas coronarios, es la menos frecuente.
2. Distribución de RestingECG (Electrocardiograma en Reposo)
Proporción: Normal: 60.1% > LVH: 20.5% > ST: 19.4%
Interpretación: La mayoría de los pacientes presentan un ECG en reposo normal. Las anomalías se dividen de manera similar entre hipertrofia ventricular izquierda (LVH) y anomalías en la onda ST-T.
Implicación para el modelo: Un ECG en reposo normal es común incluso en pacientes con enfermedad cardíaca, por lo que esta variable por sí sola podría no ser un discriminador fuerte. La presencia de LVH (un indicador de esfuerzo cardíaco crónico) y anomalías ST-T (asociadas a isquemia) son hallazgos más específicos que probablemente se correlacionen con la variable objetivo.
3. Distribución de ST_Slope (Pendiente del Segmento ST)
Proporción: Flat (Plana): 50.1% > Up (Ascendente): 43.0% > Down (Descendente): 6.9%
Interpretación: La pendiente plana es la más frecuente, seguida de cerca por la ascendente. La pendiente descendente, que es el hallazgo más preocupante, es la menos común.
Implicación para el modelo: La pendiente plana es un fuerte marcador de isquemia miocárdica. Que sea la categoría más común en este dataset de pacientes con sospecha de enfermedad es un indicador de que la prueba de esfuerzo fue muy efectiva para identificar anomalías. Se anticipa que “Flat” y “Down” serán predictores muy fuertes de enfermedad cardíaca, mientras que “Up” (normal) se asociará con la ausencia de la enfermedad.
Variables Númericas#
Se genera una tabla para visualizar algunos estadísticos descriptivos para las variables númericas.
numericas = df.select_dtypes(include=['float64', 'int64']).columns.tolist()
numericas = [col for col in numericas if df[col].nunique() > 2]
df[numericas].describe()
| Age | RestingBP | Cholesterol | MaxHR | Oldpeak | |
|---|---|---|---|---|---|
| count | 918.000000 | 918.000000 | 918.000000 | 918.000000 | 918.000000 |
| mean | 53.510893 | 132.396514 | 198.799564 | 136.809368 | 0.887364 |
| std | 9.432617 | 18.514154 | 109.384145 | 25.460334 | 1.066570 |
| min | 28.000000 | 0.000000 | 0.000000 | 60.000000 | -2.600000 |
| 25% | 47.000000 | 120.000000 | 173.250000 | 120.000000 | 0.000000 |
| 50% | 54.000000 | 130.000000 | 223.000000 | 138.000000 | 0.600000 |
| 75% | 60.000000 | 140.000000 | 267.000000 | 156.000000 | 1.500000 |
| max | 77.000000 | 200.000000 | 603.000000 | 202.000000 | 6.200000 |
Hallazgos Críticos y Posibles Errores
RestingBP (Presión Arterial en Reposo):Problema Grave: El mínimo es 0.000000 (
min: 0.000000).Interpretación: Una presión arterial de 0 mm Hg es clínicamente imposible (indicaría la muerte del paciente). Esto son valores faltantes (missing values) disfrazados.
Acción Necesaria: Identificar y manejar estos registros (imputación o eliminación).
Cholesterol (Colesterol Sérico):Problema Grave: El mínimo es 0.000000 (
min: 0.000000).Interpretación: Un nivel de colesterol de 0 mm/dl es biológicamente imposible. Al igual que con
RestingBP, estos son missing values codificados como 0.Acción Necesaria: Es crucial investigar cuántos registros están afectados y decidir una estrategia.
Análisis de Variables Numéricas “Sanas”
Age (Edad):Distribución: La edad media de los pacientes es de 53.5 años (
mean: 53.5), con una desviación estándar de 9.4 años (std: 9.4). El 50% de los pacientes (mediana) tiene entre 47 y 60 años (25%: 47,75%: 60).Interpretación: La distribución es normal, centrada en la mediana edad, que es la población típica de riesgo para enfermedades cardíacas. No se observan valores atípicos imposibles (rango de 28 a 77 años es válido).
MaxHR (Frecuencia Cardíaca Máxima):Distribución: La media es de 136.8 lpm (
mean: 136.8). Los percentiles (25%, 50%, 75%) muestran una distribución simétrica.Interpretación: El rango (60-202 lpm) es fisiológicamente posible. No hay valores evidentemente erróneos. Es una variable de buena calidad.
Oldpeak (Depresión del ST):Distribución: La media es 0.89 (
mean: 0.887), pero la mediana es 0.6 (50%: 0.6), lo que sugiere una distribución asimétrica (sesgada a la derecha). La mayoría de los valores (75%) están por debajo de 1.5.Interpretación: El valor mínimo de -2.6 es clínicamente plausible (puede indicar elevación). El valor máximo de 6.2 es alto pero posible, indicando una depresión significativa del segmento ST.
Manejo de Valores Clínicamente Imposibles#
Se identificaron valores iguales a 0 en las variables Cholesterol y RestingBP, los cuales son clínicamente imposibles. Estos valores se interpretan como missing values y se procede a reemplazarlos por NaN para un tratamiento adecuado posterior.
df['Cholesterol'] = df['Cholesterol'].replace(0, np.nan)
df['RestingBP'] = df['RestingBP'].replace(0, np.nan)
Para cuantificar la magnitud de datos ausentes en ambas columnas, se calculó el porcentaje de valores nulos mediante df.isnull().mean() * 100.
df.isnull().mean() * 100
Age 0.000000
Sex 0.000000
ChestPainType 0.000000
RestingBP 0.108932
Cholesterol 18.736383
FastingBS 0.000000
RestingECG 0.000000
MaxHR 0.000000
ExerciseAngina 0.000000
Oldpeak 0.000000
ST_Slope 0.000000
HeartDisease 0.000000
dtype: float64
Debido a que la cantidad de valores nulos en RestingBP es pequeña, eliminar los registros con valores faltantes no afectará significativamente el análisis.
df = df.dropna(subset=['RestingBP'])
Se verifica el nuevo tamaño del dataset.
df.shape
(917, 12)
Para manejar los valores faltantes en la variable Cholesterol identificados previamente, se implementó el método MICE (Multiple Imputation by Chained Equations). Esta técnica utiliza las relaciones entre las variables numéricas del dataset para imputar los valores missing de manera iterativa, preservando la estructura estadística de los datos.
df_mice = df.copy()
mice = IterativeImputer(max_iter=10, random_state=42)
df_mice[numericas] = mice.fit_transform(df_mice[numericas])
df['Cholesterol'] = df_mice['Cholesterol']
Se verifican los cambios realizados.
df.isnull().mean() * 100
Age 0.0
Sex 0.0
ChestPainType 0.0
RestingBP 0.0
Cholesterol 0.0
FastingBS 0.0
RestingECG 0.0
MaxHR 0.0
ExerciseAngina 0.0
Oldpeak 0.0
ST_Slope 0.0
HeartDisease 0.0
dtype: float64
Una vez aplicada las imputaciones se procede a verificar las estadísticas descriptivas de las variables tratadas para evaluar cómo ha impactado el proceso en su distribución y asegurar la conservación de sus propiedades estadísticas fundamentales.
df[["Cholesterol", "RestingBP"]].describe()
| Cholesterol | RestingBP | |
|---|---|---|
| count | 917.000000 | 917.000000 |
| mean | 244.638971 | 132.540894 |
| std | 53.383904 | 17.999749 |
| min | 85.000000 | 80.000000 |
| 25% | 214.000000 | 120.000000 |
| 50% | 242.824123 | 130.000000 |
| 75% | 267.000000 | 140.000000 |
| max | 603.000000 | 200.000000 |
1. Estrategia Aplicada
RestingBP: Al tener menos del 1% de valores nulos (una sola observación), se optó por la eliminación de dicho registro. Esta es una estrategia conservadora y válida que evita introducir sesgo por imputación cuando el impacto en el tamaño de la muestra es mínimo.Cholesterol: Al tener un porcentaje significativo de valores nulos (18%), se aplicó el método MICE (Multiple Imputation by Chained Equations), una técnica avanzada que preserva la estructura estadística de los datos.
2. Evaluación de RestingBP Post-Eliminación
Estabilidad de la distribución: Las estadísticas descriptivas se mantienen prácticamente idénticas a las originales.
Rango clínico válido: El mínimo de 80 mm Hg y máximo de 200 mm Hg son valores fisiológicamente posibles, indicando que la limpieza fue efectiva.
Conclusión: La eliminación del único valor nulo no impactó la representatividad de la variable.
3. Evaluación de Cholesterol Post-Imputación MICE
Cambio en la tendencia central: La media aumentó de 198.8 a 244.6 mg/dl, lo que sugiere que los valores faltantes correspondían predominantemente a pacientes con niveles de colesterol más altos.
Distribución más realista:
El mínimo de 85 mg/dl es clínicamente posible (se eliminaron los valores 0 imposibles).
La mediana de 242.8 mg/dl indica que la mayoría de los pacientes se encuentran en rango de colesterol límite alto/alto.
El máximo de 603 mg/dl representa casos de hipercolesterolemia severa real.
4. Impacto General en la Calidad del Dataset
Integridad: Ambas variables ahora tienen 0 valores nulos.
Validez clínica: Todos los valores están dentro de rangos fisiológicamente posibles.
Consistencia: Se mantuvo la variabilidad natural de las mediciones clínicas.
Preparación para modelado: El dataset está listo para proceder con las siguientes etapas de análisis.
Conclusión: Las estrategias de tratamiento de valores faltantes fueron apropiadas y efectivas, produciendo un dataset más robusto y clínicamente válido para el modelado predictivo.
Se genera un histograma y un boxplot para visualizar la distribución de frecuencias e identificar outliers (puntos fuera de los bigotes) y la dispersión de los datos. Además se evalua:
Asimetría:
skew = 0: Distribución simétrica (valores aceptables skew \(\in (-1,1)\)).skew > 0: Mayor peso en la cola izquierda de la distribución (sesgo positivo).skew < 0: Mayor peso en la cola derecha de la distribución (sesgo negativo).
Kurtosis: Determina si una distribución tiene colas gruesas con respecto a la distribución normal. Proporciona información sobre la forma de una distribución de frecuencias.
kurtosis = 3: se denomina mesocúrtica (distribución normal).kurtosis < 3: se denomina platicúrtica (distribución con colas menos gruesas que la normal).kurtosis > 3: se denomina leptocúrtica (distribución con colas más gruesas que la normal) y significa que trata de producir más valores atípicos que la distribución normal.
Coeficiente de Variación: Es una medida estadística que se utiliza para evaluar la variabilidad relativa de una muestra en relación con su media. Se calcula como la desviación estándar de los datos dividida por la media, y se expresa como un porcentaje multiplicado por 100 para facilitar su interpretación.
Para el número de Bins se utiliza la Regla de Rice.
for col in numericas:
print(f"\n=== {col.upper()} ===")
print('Skew:', round(df[col].skew(), 2))
print('Kurtosis: ', round(df[col].kurtosis(), 2))
coef_variacion = (df[col].std() / df[col].mean()) * 100
print('Coeficiente de Variación: ', round(coef_variacion, 2), '%')
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.hist(df[col], bins=10, edgecolor="black", color=colors[0])
plt.title(f'Histograma de {col}')
plt.subplot(1, 2, 2)
sns.boxplot(x=df[col], color=colors[1])
plt.title(f'Boxplot de {col}')
plt.tight_layout()
plt.show()
=== AGE ===
Skew: -0.2
Kurtosis: -0.39
Coeficiente de Variación: 17.64 %
=== RESTINGBP ===
Skew: 0.61
Kurtosis: 0.79
Coeficiente de Variación: 13.58 %
=== CHOLESTEROL ===
Skew: 1.37
Kurtosis: 6.22
Coeficiente de Variación: 21.82 %
=== MAXHR ===
Skew: -0.14
Kurtosis: -0.45
Coeficiente de Variación: 18.62 %
=== OLDPEAK ===
Skew: 1.02
Kurtosis: 1.2
Coeficiente de Variación: 120.33 %
1. Age (Edad)
Skew: -0.2 → Distribución ligeramente asimétrica negativa. La cola izquierda es más larga, indicando una concentración ligeramente mayor de pacientes en edades más avanzadas, pero la asimetría es mínima (casi simétrica).
Kurtosis: -0.39 → Distribución platicúrtica (más plana) que la normal. Los datos están menos concentrados alrededor de la media y tienen colas más ligeras.
Coeficiente de Variación: 17.64% → Dispersión moderada y saludable. La variabilidad relativa es aceptable para análisis estadísticos.
2. Cholesterol (Colesterol)
Skew: 1.37 → Asimetría fuertemente positiva. Indicando una cola extendida hacia la derecha.
Kurtosis: 6.22 → Distribución extremadamente leptocúrtica (muy picada). Los datos están muy concentrados alrededor de la media, sugiriendo posible sobre-ajuste en la imputación.
Coeficiente de Variación: 21.82% → Dispersión moderada. Indicando datos homogéneos y consistentes.
3. MaxHR (Frecuencia Cardíaca Máxima)
Skew: -0.14 → Distribución casi perfectamente simétrica.
Kurtosis: -0.45 → Ligera platocurtosis, indicando una distribución saludable sin valores extremos pronunciados.
Coeficiente de Variación: 18.62% → Dispersión moderada y consistente. Se mantiene como una de las variables mejor comportadas del dataset.
4. Oldpeak (Depresión del ST)
Skew: 1.02 → Fuertemente asimétrica positiva. La mayoría de los valores están concentrados en el rango bajo (0-1.5) con una cola hacia valores más altos.
Kurtosis: 1.2 → Distribución leptocúrtica, con valores más concentrados y presencia de algunos valores extremos positivos.
Coeficiente de Variación: 120.33% → Dispersión muy alta. Indica una gran variabilidad relativa en las mediciones de depresión del ST entre pacientes.
5. RestingBP (Presión Arterial en Reposo)
Skew: 0.61 → Distribución moderadamente asimétrica positiva. La cola derecha es más larga, indicando la presencia de algunos valores de presión arterial elevada, pero la asimetría es manejable.
Kurtosis: 0.79 → Distribución levemente leptocúrtica. Los datos presentan una concentración moderada alrededor de la media con colas ligeramente más pesadas que la distribución normal.
Coeficiente de Variación: 13.58% → Baja dispersión relativa. Los valores de presión arterial muestran consistencia y estabilidad, con variabilidad controlada que es típica en mediciones fisiológicas de presión arterial.
Análisis de Valores Atípicos#
Para la identificación de valores atípicos en las variables númericas, se aplicó el método del rango intercuartílico (IQR).
for col in numericas:
print(f"\n=== {col.upper()} ===")
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
limite_inferior = Q1 - 1.5 * IQR
limite_superior = Q3 + 1.5 * IQR
outliers = df[(df[col] < limite_inferior) | (df[col] > limite_superior)]
print(f"Número de outliers: {len(outliers)}")
print(f"Porcentaje de outliers: {len(outliers)/len(df)*100:.2f}%")
=== AGE ===
Número de outliers: 0
Porcentaje de outliers: 0.00%
=== RESTINGBP ===
Número de outliers: 27
Porcentaje de outliers: 2.94%
=== CHOLESTEROL ===
Número de outliers: 41
Porcentaje de outliers: 4.47%
=== MAXHR ===
Número de outliers: 2
Porcentaje de outliers: 0.22%
=== OLDPEAK ===
Número de outliers: 16
Porcentaje de outliers: 1.74%
1. Age (Edad)
Número de outliers: 0 → Porcentaje: 0.00%
Interpretación: La distribución de edad es extremadamente limpia y consistente. No se detectan valores atípicos según el criterio IQR.
2. RestingBP (Presión Arterial en Reposo)
Número de outliers: 27 → Porcentaje: 2.94%
Interpretación: Presencia de outliers moderados. Probablemente corresponden a pacientes con hipertensión severa (valores muy altos) o hipotensión (valores muy bajos).
3. Cholesterol (Colesterol)
Número de outliers: 41 → Porcentaje: 4.47%
Interpretación: Cantidad significativa de outliers, principalmente valores altos. Esto es esperable después de la imputación con MICE y refleja pacientes con hipercolesterolemia real.
4. MaxHR (Frecuencia Cardíaca Máxima)
Número de outliers: 2 → Porcentaje: 0.22%
Interpretación: Muy pocos outliers. Posiblemente corresponden a pacientes con frecuencias cardíacas máximas excepcionalmente bajas o altas durante el ejercicio.
5. Oldpeak (Depresión del ST)
Número de outliers: 16 → Porcentaje: 1.74%
Interpretación: Outliers moderados y esperables. Corresponden a pacientes con depresión significativa del segmento ST, lo cual es clínicamente relevante para enfermedad cardíaca.
Manejo de Valores Atípicos mediante Winsorization#
Para mitigar el impacto de valores extremos en las variables numéricas identificadas previamente en el análisis de outliers, se aplicó la técnica de winsorization. Este método preserva los casos atípicos reemplazando los valores en los percentiles extremos superior e inferior por los valores en un límite especificado, reduciendo así la distorsión en los modelos sin eliminar registros valiosos. Los límites se establecieron equilibrando la retención de información clínicamente relevante con la estabilidad estadística del modelo.
df['RestingBP'] = winsorize(df['RestingBP'], limits=[0.02, 0.02])
df['Cholesterol'] = winsorize(df['Cholesterol'], limits=[0.04, 0.04])
df['Oldpeak'] = winsorize(df['Oldpeak'], limits=[0.01, 0.01])
Se verifican los cambios realizados.
atipicos =["RestingBP","Cholesterol","Oldpeak"]
df[atipicos].describe()
| RestingBP | Cholesterol | Oldpeak | |
|---|---|---|---|
| count | 917.000000 | 917.000000 | 917.000000 |
| mean | 132.496183 | 243.080629 | 0.888332 |
| std | 17.422543 | 43.281662 | 1.029460 |
| min | 100.000000 | 164.000000 | -0.500000 |
| 25% | 120.000000 | 214.000000 | 0.000000 |
| 50% | 130.000000 | 242.824123 | 0.600000 |
| 75% | 140.000000 | 267.000000 | 1.500000 |
| max | 180.000000 | 340.000000 | 4.000000 |
1. RestingBP (Presión arterial en reposo)
Mínimo: Aumentó de 80 a 100 mm Hg.
Máximo: Disminuyó de 200 a 180 mm Hg.
Desviación estándar: Reducción leve de 17.99 a 17.42.
Interpretación: La winsorización recortó valores en ambos extremos de la distribución. El nuevo mínimo (100 mm Hg) corrige valores de hipotensión extrema, mientras el nuevo máximo (180 mm Hg) limita casos de hipertensión severa. La distribución mantiene su variabilidad clínicamente relevante pero es más robusta para modelado.
2. Cholesterol (Colesterol sérico)
Mínimo: Aumentó significativamente de 85 a 164 mg/dl.
Máximo: Disminuyó drásticamente de 603 a 340 mg/dl.
Desviación estándar: Reducción notable de 53.38 a 43.28.
Interpretación: La winsorización controló efectivamente los valores extremos, especialmente los superiores (hipercolesterolemia severa >340 mg/dl). Esto reduce el sesgo positivo extremo de la variable, creando una distribución más compacta y manejable para modelos sensibles a outliers, mientras preserva el rango clínicamente significativo de colesterol elevado.
3. Oldpeak (Depresión del segmento ST)
Mínimo: Aumentó de -2.6 a -0.5.
Máximo: Reducción significativa de 6.2 a 4.0.
Desviación estándar: Leve disminución de 1.07 a 1.03.
Interpretación: Los valores extremos de depresión severa del segmento ST (>4.0) fueron ajustados al límite superior winsorizado. Esto es clínicamente apropiado, ya que valores superiores a 4.0 mm representan anomalías electrocardiográficas graves y su reducción controla su influencia desproporcionada en el modelo, manteniendo su valor predictivo crítico dentro de rangos clínicamente relevantes.
Verificación Visual del Efecto de la Winsorization#
Para evaluar visualmente el impacto de la winsorization en la distribución y dispersión de las variables tratadas (RestingBP, Cholesterol, Oldpeak), se generaron histogramas y diagramas de caja (boxplots). Estas visualizaciones permiten contrastar las distribuciones antes y después del tratamiento, confirmando la reducción de valores extremos mientras se mantiene la estructura central de los datos, asegurando así la preparación adecuada para el modelado. Además se evalua nuevamente:
Asimetría:
skew = 0: Distribución simétrica (valores aceptables skew \(\in (-1,1)\)).skew > 0: Mayor peso en la cola izquierda de la distribución (sesgo positivo).skew < 0: Mayor peso en la cola derecha de la distribución (sesgo negativo).
Kurtosis: Determina si una distribución tiene colas gruesas con respecto a la distribución normal. Proporciona información sobre la forma de una distribución de frecuencias.
kurtosis = 3: se denomina mesocúrtica (distribución normal).kurtosis < 3: se denomina platicúrtica (distribución con colas menos gruesas que la normal).kurtosis > 3: se denomina leptocúrtica (distribución con colas más gruesas que la normal) y significa que trata de producir más valores atípicos que la distribución normal.
Coeficiente de Variación: Es una medida estadística que se utiliza para evaluar la variabilidad relativa de una muestra en relación con su media. Se calcula como la desviación estándar de los datos dividida por la media, y se expresa como un porcentaje multiplicado por 100 para facilitar su interpretación.
Para el número de Bins se utiliza la Regla de Rice.
for col in atipicos:
print(f"\n=== {col.upper()} ===")
print('Skew:', round(df[col].skew(), 2))
print('Kurtosis: ', round(df[col].kurtosis(), 2))
coef_variacion = (df[col].std() / df[col].mean()) * 100
print('Coeficiente de Variación: ', round(coef_variacion, 2), '%')
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.hist(df[col], bins=10, edgecolor="black", color=colors[0])
plt.title(f'Histograma de {col}')
plt.subplot(1, 2, 2)
sns.boxplot(x=df[col], color=colors[1])
plt.title(f'Boxplot de {col}')
plt.tight_layout()
plt.show()
=== RESTINGBP ===
Skew: 0.51
Kurtosis: 0.13
Coeficiente de Variación: 13.15 %
=== CHOLESTEROL ===
Skew: 0.32
Kurtosis: -0.19
Coeficiente de Variación: 17.81 %
=== OLDPEAK ===
Skew: 0.97
Kurtosis: 0.19
Coeficiente de Variación: 115.89 %
1. RestingBP (Presión arterial en reposo)
Skewness (0.51): El sesgo se redujo levemente (de 0.61 a 0.51), indicando una distribución más simétrica. La winsorización equilibró efectivamente la influencia de valores extremos en ambos extremos de la distribución.
Kurtosis (0.13): La curtosis disminuyó moderadamente (de 0.79 a 0.13), acercándose a una distribución mesocúrtica. Esto refleja una reducción en la concentración de valores extremos y una distribución más normal.
Coeficiente de Variación (13.15%): La dispersión relativa disminuyó (de 13.58 % a 13.15%), confirmando que la variable es más estable y consistente después del tratamiento, manteniendo su variabilidad clínicamente relevante.
2. Cholesterol (Colesterol sérico)
Skewness (0.32): El sesgo se redujo drásticamente (de 1.37 a 0.32), indicando una distribución mucho más simétrica. La winsorización corrigió eficientemente la asimetría positiva extrema causada por los valores de hipercolesterolemia severa.
Kurtosis (-0.19): La curtosis disminuyó notablemente (de 6.22 a -0.19), transformando la distribución de extremadamente leptocúrtica a ligeramente platicúrtica. Esto refleja una eliminación completa de los valores atípicos que causaban el exceso de curtosis.
Coeficiente de Variación (17.81%): La dispersión relativa se redujo significativamente (de 21.82% a 17.81%), indicando que la variabilidad de los datos es ahora más homogénea y menos influenciada por outliers extremos.
3. Oldpeak (Depresión del segmento ST)
Skewness (0.97): El sesgo disminuyó levemente (de 1.02 a 0.97), pero sigue siendo moderadamente positivo. Esto era esperado debido a la naturaleza inherentemente sesgada de esta variable clínica, donde la mayoría de los pacientes presentan depresiones leves o nulas del ST.
Kurtosis (0.19): La curtosis se redujo (de 1.20 a 0.19), acercándose a una distribución mesocúrtica. Esto indica que los valores extremos superiores fueron controlados efectivamente.
Coeficiente de Variación (115.89%): La dispersión relativa disminuyó ligeramente (de 120.33% a 115.89%), pero sigue siendo muy alta. Esto confirma que
Oldpeakmantiene su alta variabilidad inherente, característica de las mediciones de depresión del segmento ST en población con sospecha de enfermedad cardíaca.
Análisis Bivariado#
El análisis bivariado es la segunda fase del análisis exploratorio de datos. Se enfoca en las relaciones entre dos variables para obtener datos estadísticos sobre sus influencias mutuas.
Variables Binarias vs HeartDisease#
Se generan graficos de barras para analizar la relación entre un conjunto de variables binarias y la presencia de enfermedad cardíaca con el fin de identificar qué variables binarias están asociadas a mayores proporciones de diagnóstico positivos en el conjunto de datos.
plt.figure(figsize=(10, 5))
for i, col in enumerate(binarias, 1):
if col != "HeartDisease":
plt.subplot(1, 3, i)
ax = sns.barplot(
x=col,
y='HeartDisease',
data=df,
palette='Set2',
errorbar=None
)
plt.title(f'HeartDisease vs {col}')
plt.ylabel('Proporción de Presencia de Enfermedad Cardíaca')
for container in ax.containers:
ax.bar_label(container, fmt="%.2f")
plt.tight_layout()
plt.show()
1. HeartDisease vs Sex (Enfermedad Cardíaca vs Sexo)
Relación: Fuerte correlación con claro dimorfismo sexual. La prevalencia de enfermedad cardíaca es significativamente mayor en hombres.
Hallazgos Clave:
Masculino (M): 63% - Casi dos tercios de los pacientes hombres en el dataset tienen enfermedad cardíaca.
Femenino (F): 26% - Solo una cuarta parte de las mujeres presentan la enfermedad.
Implicación para el Modelo: El sexo masculino es un factor de riesgo mayor en este dataset. La variable
Sexserá un predictor importante, pero es crucial recordar el desbalance en la muestra (79% hombres vs 21% mujeres). El modelo podría tener un mejor rendimiento predictivo en hombres debido a esta sobrerrepresentación.
2. HeartDisease vs FastingBS (Enfermedad Cardíaca vs Glucemia en Ayunas)
Relación: Correlación moderadamente fuerte. La glucemia elevada está asociada con una mayor prevalencia de enfermedad cardíaca.
Hallazgos Clave:
FastingBS > 120 mg/dl (1): 79% - Aproximadamente dos de cada tres pacientes con glucemia elevada tienen enfermedad cardíaca.
FastingBS ≤ 120 mg/dl (0): 48% - Aproximadamente la mitad de los pacientes con glucemia normal también presentan la enfermedad.
Implicación para el Modelo: La diabetes o prediabetes (
FastingBS = 1) es un factor de riesgo claro, elevando la probabilidad de enfermedad cardíaca en un 31% en comparación con los pacientes con glucemia normal. Será un predictor útil, aunque no tan determinante como otras variables.
3. HeartDisease vs ExerciseAngina (Enfermedad Cardíaca vs Angina Inducida por Ejercicio)
Relación: Correlación extremadamente fuerte. La presencia de angina durante el ejercicio es un indicador poderoso de enfermedad cardíaca.
Hallazgos Clave:
ExerciseAngina = Sí (Y): 85% - La gran mayoría de los pacientes que reportan angina durante la prueba de esfuerzo tienen enfermedad cardíaca.
ExerciseAngina = No (N): 35% - Solo alrededor de un tercio de los que no la reportan están enfermos.
Implicación para el Modelo:
ExerciseAnginaes, junto conST_Slope, uno de los predictores más poderosos del dataset. La capacidad del modelo para identificar correctamente a los pacientes que experimentan angina con el ejercicio será fundamental para su precisión general.
Conclusión General y Orden de Importancia Predictiva:
Basado en este análisis bivariado, el poder predictivo de estas variables binarias, de mayor a menor, es:
ExerciseAngina: Discriminador excepcional. La presencia de angina durante la prueba de esfuerzo es un signo de alarma mayor.Sex: Predictor muy fuerte, mostrando una disparidad muy marcada en la prevalencia de la enfermedad entre hombres y mujeres en esta cohorte.FastingBS: Predictor moderado, pero clínicamente relevante, que refuerza la conocida asociación entre la disglucemia y la enfermedad cardiovascular.
En resumen, el perfil de mayor riesgo en este dataset es: Hombre, con glucemia elevada, que desarrolla angina durante la prueba de esfuerzo
Variables Categóricas vs HeartDisease#
Se generan graficos de barras para analizar la relación entre variables categóricas y la presencia de enfermedad cardíaca en la población estudiada. Con el fin de identificar qué categorías dentro de cada variable presentan mayores proporciones de diagnósticos positivos, lo que podría indicar posibles factores de riesgo asociados.
for col in categoricas:
print(f"\n=== {col.upper()} ===")
plt.figure(figsize=(10, 5))
order = (
df.groupby(col)['HeartDisease']
.mean()
.sort_values(ascending=False)
.index
)
ax = sns.barplot(
x=col,
y='HeartDisease',
data=df,
palette='Set2',
errorbar=None,
order=order
)
plt.title(f'HeartDisease vs {col}')
plt.ylabel('Proporción de Presencia de Enfermedad Cardíaca')
for container in ax.containers:
ax.bar_label(container, fmt="%.2f")
plt.tight_layout()
plt.show()
=== CHESTPAINTYPE ===
=== RESTINGECG ===
=== ST_SLOPE ===
1. HeartDisease vs ChestPainType (Enfermedad Cardíaca vs Tipo de Dolor Torácico)
Relación: Fuerte correlación. La presencia de enfermedad cardíaca varía drásticamente según el tipo de dolor torácico.
Hallazgos Clave:
ASY (Asintomático): 79% - La gran mayoría de pacientes asintomáticos tienen enfermedad cardíaca. Este es el hallazgo más crítico, ya que resalta la naturaleza “silenciosa” de la condición en esta población.
TA (Angina Típica): 43% - Menos de la mitad de los pacientes con el dolor clásicamente asociado a problemas coronarios tienen la enfermedad.
ATA (Angina Atípica): 14% y NAP (Dolor No Anginoso): 35% - Presentan las proporciones más bajas de enfermedad.
Implicación para el Modelo: La variable
ChestPainTypeserá un predictor extremadamente poderoso. El valorASYes, de forma contraintuitiva, el indicador de riesgo más alto. Esto sugiere que el modelo aprenderá que la ausencia de síntomas típicos en este contexto específico es una señal de alarma.
2. HeartDisease vs RestingECG (Enfermedad Cardíaca vs Electrocardiograma en Reposo)
Relación: Correlación moderada a débil. Las anomalías en el ECG en reposo muestran una prevalencia de enfermedad solo ligeramente mayor.
Hallazgos Clave:
ST (Anomalía ST-T): 66% - La anomalía más específica para isquemia muestra la mayor proporción de enfermedad.
LVH (Hipertrofia Ventricular Izquierda): 56% - Un marcador de esfuerzo cardíaco crónico también está asociado a una mayor prevalencia.
Normal: 52% - Sorprendentemente, más de la mitad de los pacientes con un ECG en reposo normal tienen enfermedad cardíaca.
Implicación para el Modelo: Un ECG en reposo normal no descarta la enfermedad. Si bien las anomalías (ST y LVH) aumentan la probabilidad, la discriminación no es perfecta. Esta variable probablemente tendrá una importancia moderada en el modelo, complementando a predictores más fuertes.
3. HeartDisease vs ST_Slope (Enfermedad Cardíaca vs Pendiente del ST en Ejercicio)
Relación: Correlación extremadamente fuerte y clínicamente esperada. Esta es probablemente la variable predictiva individual más importante.
Hallazgos Clave:
Down (Descendente): 78% - Casi todos los pacientes con esta anomalía severa tienen enfermedad cardíaca.
Flat (Plana): 83% - La pendiente plana, un marcador de isquemia, tiene una asociación muy alta con la enfermedad.
Up (Ascendente): 20% - Solo 1 de cada 5 pacientes con una respuesta normal a la prueba de esfuerzo tiene la enfermedad.
Implicación para el Modelo:
ST_Slopeserá un predictor de primera clase. El modelo dependerá en gran medida de esta variable para distinguir entre pacientes sanos y enfermos. La separación casi perfecta entre “Up” (mayormente sano) y “Flat/Down” (mayormente enfermo) la convierte en una característica fundamental.
Conclusión General y Orden de Importancia Predictiva
Basado en el análisis bivariado, el poder predictivo de estas variables categóricas, de mayor a menor, es:
ST_Slope: Discriminador casi perfecto. La variable categórica más importante.ChestPainType: Predictor muy fuerte, especialmente por el alto riesgo asociado a ser asintomático (ASY).RestingECG: Predictor más débil, pero aún valioso, especialmente el hallazgo de anomalías en la onda ST-T.
Este análisis confirma que las pruebas de esfuerzo (ST_Slope) y la presentación clínica atípica (ChestPainType = ASY) son los factores clave que el modelo utilizará para predecir la enfermedad cardíaca en este dataset.
Variables Númericas vs HeartDisease#
Con el objetivo de identificar la relación entre las variables numéricas predictoras y la variable objetivo (HeartDisease), se genera una serie de diagramas de caja (boxplots). Esta visualización permite comparar la distribución (mediana, rango intercuartílico) de cada variable numérica segmentada por la clase de diagnóstico, facilitando la observación de diferencias significativas entre los grupos que podrían ser patrones predictivos clave.
for col in numericas:
print(f"\n=== {col.upper()} ===")
plt.figure(figsize=(10, 5))
sns.boxplot(
x=col,
y='HeartDisease',
data=df,
palette='Set2',
orient='h'
)
plt.title(f'HeartDisease vs {col}')
plt.tight_layout()
plt.show()
=== AGE ===
=== RESTINGBP ===
=== CHOLESTEROL ===
=== MAXHR ===
=== OLDPEAK ===
1. HeartDisease vs Age (Enfermedad Cardíaca vs Edad)
Relación: Correlación positiva moderada. Se observa una tendencia clara donde la prevalencia de enfermedad cardíaca aumenta con la edad.
Hallazgos Clave:
Pacientes jóvenes (<45 años): Baja densidad de puntos rojos (enfermedad), predominando los verdes (sanos).
Pacientes de mediana edad (45-60 años): Mezcla más equilibrada entre ambos grupos.
Pacientes mayores (>60 años): Clara predominancia de puntos rojos (enfermos).
Implicación para el Modelo: La
Ageserá un predictor consistente y confiable. El modelo aprenderá que el riesgo aumenta progresivamente con la edad, lo cual es epidemiológicamente correcto.
2. HeartDisease vs RestingBP (Enfermedad Cardíaca vs Presión Arterial en Reposo)
Relación: Correlación muy débil o nula. La distribución de puntos rojos y verdes es bastante uniforme a lo largo de todo el rango de presión arterial.
Hallazgos Clave:
No se aprecia una concentración clara de casos de enfermedad en los valores altos de presión arterial.
Hay muchos pacientes con presión arterial normal (verdes alrededor de 120-130 mmHg) y también muchos con hipertensión (rojos por encima de 140 mmHg).
Implicación para el Modelo:
RestingBPprobablemente tendrá una baja importancia relativa en el modelo predictivo final. Por sí sola, no es un buen discriminador.
3. HeartDisease vs Cholesterol (Enfermedad Cardíaca vs Colesterol)
Relación: Correlación positiva débil a moderada. Existe una ligera tendencia donde los niveles más altos de colesterol se asocian con más casos de enfermedad cardíaca.
Hallazgos Clave:
En el rango bajo de colesterol (175-225 mg/dl) hay una mezcla de pacientes sanos y enfermos.
A medida que el colesterol aumenta (especialmente por encima de ~250 mg/dl), la densidad de puntos rojos (enfermos) parece incrementarse.
Implicación para el Modelo:
Cholesterolaportará información al modelo, pero no será uno de los predictores principales. Su poder predictivo es limitado de forma aislada.
4. HeartDisease vs MaxHR (Enfermedad Cardíaca vs Frecuencia Cardíaca Máxima)
Relación: Correlación negativa fuerte y clara. Esta es una de las relaciones más evidentes entre las variables numéricas.
Hallazgos Clave:
Alta MaxHR (>150 bpm): Predominio absoluto de puntos verdes (sanos).
Baja MaxHR (<120 bpm): Predominio absoluto de puntos rojos (enfermos).
Existe un punto de separación alrededor de los 130-140 bpm donde la proporción se invierte.
Implicación para el Modelo:
MaxHRserá un predictor de primera clase, extremadamente poderoso. La incapacidad para alcanzar una frecuencia cardíaca alta durante el ejercicio es un fuerte indicador de enfermedad coronaria subyacente.
5. HeartDisease vs Oldpeak (Depresión del ST)
Relación: Correlación positiva muy fuerte. La relación es casi lineal: a mayor depresión del ST, mayor probabilidad de enfermedad.
Hallazgos Clave:
Oldpeak = 0: Predominan los puntos verdes (sanos).
Oldpeak > 1: Predominan abrumadoramente los puntos rojos (enfermos).
Casi no hay puntos verdes para valores de Oldpeak superiores a 2.
Implicación para el Modelo:
Oldpeakes, junto conMaxHRyST_Slope, uno de los predictores numéricos más importantes. Será una variable fundamental para que el modelo identifique a los pacientes con isquemia miocárdica inducible.
Conclusión General y Orden de Importancia Predictiva
Basado en el análisis visual bivariado, el poder predictivo de estas variables numéricas, de mayor a menor, es:
Oldpeak: Relación casi determinista con la enfermedad para valores altos.MaxHR: Fuerte discriminador, con una correlación negativa muy clara.Age: Predictor consistente con una tendencia positiva evidente.Cholesterol: Predictor débil, con una tendencia positiva leve.RestingBP: Predictor muy débil o nulo en este análisis visual.
En resumen, las variables derivadas de la prueba de esfuerzo (Oldpeak y MaxHR) son, con diferencia, los predictores numéricos más potentes para la enfermedad cardíaca en este dataset.
Para comprender la distribución y la solapación de cada variable numérica entre los pacientes sanos y aquellos con diagnóstico positivo, se generaron histogramas comparativos. Estos gráficos permiten visualizar diferencias en la forma de la distribución, tendencias centrales y dispersión entre las dos clases, lo que ayuda a identificar qué variables tienen un mayor poder discriminatorio.
for col in numericas:
print(f"\n=== {col.upper()} ===")
plt.figure(figsize=(10, 5))
sns.histplot(
data=df,
x=col,
hue="HeartDisease",
stat="probability",
bins=10,
palette="Set2",
element="poly",
common_norm=False
)
plt.title(f'HeartDisease vs {col}')
plt.xlabel(col)
plt.ylabel('Proporción de Presencia de Enfermedad Cardíaca')
plt.tight_layout()
plt.show()
=== AGE ===
=== RESTINGBP ===
=== CHOLESTEROL ===
=== MAXHR ===
=== OLDPEAK ===
1. Age (Edad) según HeartDisease
Relación: Diferencias claras en la distribución por edad.
Hallazgos Clave:
Sin Enfermedad (0): La distribución es más joven, con su pico alrededor de los 50-55 años.
Con Enfermedad (1): La distribución se desplaza hacia la derecha, con un pico alrededor de los 55-60 años y una cola más larga hacia edades avanzadas.
Implicación para el Modelo: La edad es un factor de riesgo consistente. El modelo aprenderá que los pacientes mayores tienen mayor probabilidad de enfermedad cardíaca.
2. RestingBP (Presión Arterial en Reposo) según HeartDisease
Relación: Distribuciones muy similares, diferencia mínima.
Hallazgos Clave:
Ambas distribuciones (con y sin enfermedad) son casi superpuestas.
No se aprecia un desplazamiento claro ni diferencias significativas en la forma de las distribuciones.
Implicación para el Modelo:
RestingBPpor sí sola tendrá un poder discriminatorio muy bajo. Probablemente será una de las variables menos importantes en el modelo predictivo.
3. Cholesterol (Colesterol) según HeartDisease
Relación: Diferencias moderadas en la distribución.
Hallazgos Clave:
Sin Enfermedad (0): Distribución ligeramente desplazada hacia la izquierda, con mayor densidad en valores alrededor de 220-240 mg/dl.
Con Enfermedad (1): Distribución más plana y extendida hacia la derecha, con mayor frecuencia relativa en valores superiores a 240 mg/dl.
Implicación para el Modelo: El colesterol aportará señal discriminativa al modelo, especialmente para identificar pacientes con niveles muy elevados (>310 mg/dl) que están asociados con mayor riesgo.
4. MaxHR (Frecuencia Cardíaca Máxima) según HeartDisease
Relación: Diferencias muy marcadas y claras.
Hallazgos Clave:
Sin Enfermedad (0): Distribución desplazada hacia la derecha, con un pico pronunciado alrededor de 150-165 lpm.
Con Enfermedad (1): Distribución desplazada hacia la izquierda, con un pico alrededor de 120-130 lpm.
Implicación para el Modelo:
MaxHRserá un predictor extremadamente poderoso. La incapacidad para alcanzar frecuencias cardíacas altas durante el ejercicio es un fuerte indicador de enfermedad coronaria.
5. Oldpeak (Depresión del ST) según HeartDisease
Relación: Diferencias extremadamente marcadas.
Hallazgos Clave:
Sin Enfermedad (0): Distribución muy concentrada cerca de 0, con rápida disminución.
Con Enfermedad (1): Distribución mucho más extendida hacia la derecha, con una moda alrededor de 1.0 y frecuencia significativa hasta valores de 2.0-3.0.
Implicación para el Modelo:
Oldpeakes probablemente el predictor numérico más fuerte. Cualquier valor significativamente mayor que 0 aumenta drásticamente la probabilidad de enfermedad cardíaca.
En resumen, las mediciones de la prueba de esfuerzo (Oldpeak y MaxHR) son, con diferencia, los predictores numéricos más potentes, mientras que la presión arterial en reposo aporta muy poca información discriminatoria por sí sola.
Análisis Multivariado#
Para identificar relaciones lineales entre las variables del Dataset, se generó una matriz de correlación mediante un heatmap. Este gráfico muestra el coeficiente de correlación de Pearson (rango: -1 a 1) entre cada par de variables, utilizando una escala de colores (azul para correlaciones negativas, rojo para positivas). Los valores numéricos anotados permiten evaluar posibles dependencias entre variables.
binarias_cat = ["Sex", "ExerciseAngina"]
binarias_num = ["FastingBS", "HeartDisease"]
encoder_heatmap = OneHotEncoder(handle_unknown="ignore")
encoded_heatmap = encoder_heatmap.fit_transform(df[categoricas + binarias_cat])
df_heatmap = pd.DataFrame(
encoded_heatmap.toarray(),
columns=encoder_heatmap.get_feature_names_out(categoricas + binarias_cat),
index=df.index
)
df_heatmap = pd.concat([df[numericas + binarias_num], df_heatmap], axis=1)
plt.figure(figsize=(12, 12))
sns.heatmap(
df_heatmap.corr(),
annot=True,
fmt=".2f",
cmap="coolwarm",
vmin=-1, vmax=1,
linewidths=0.5
)
plt.title("Matriz de Correlación")
plt.tight_layout()
plt.show()
La variable de interés principal es HeartDisease (enfermedad cardíaca), por lo que nos enfocamos en su relación con otras variables.
Correlaciones con HeartDisease:
ChestPainType_ASY: 0.52 → Correlación positiva alta. Si el tipo de dolor en el pecho es asintomático (ASY), hay alta probabilidad de enfermedad cardíaca.
ExerciseAngina_Y: 0.50 → Si el paciente tiene angina inducida por ejercicio, hay alta correlación con enfermedad cardíaca.
Oldpeak: 0.42 → A mayor depresión del segmento ST inducida por el ejercicio, mayor probabilidad de enfermedad.
ST_Slope_Flat: 0.55 → Una pendiente plana en el ECG también tiene fuerte correlación con enfermedad cardíaca.
ChestPainType_ATA: -0.40 → Tipo de dolor atípico está inversamente relacionado con enfermedad cardíaca.
MaxHR: -0.40 → A mayor frecuencia cardíaca máxima alcanzada, menor probabilidad de enfermedad.
ST_Slope_Up: -0.62 → Pendiente ascendente del ST tiene correlación negativa con enfermedad.
ExerciseAngina_N: -0.50.
Otras relaciones interesantes:
MaxHR y Age: -0.38 → A mayor edad, menor frecuencia cardíaca máxima.
Oldpeak y ST_Slope_Flat: 0.30 → Tiene sentido, ya que una mayor depresión del ST puede relacionarse con un tipo específico de pendiente.
ExerciseAngina_Y y Oldpeak: 0.42 → Mayor oldpeak asociado con angina inducida por ejercicio.
Variables más correlacionadas positivamente con la enfermedad cardíaca#
Variable |
Correlación con HeartDisease |
|---|---|
ST_Slope_Flat |
0.55 |
ChestPainType_ASY |
0.52 |
ExerciseAngina_Y |
0.50 |
Oldpeak |
0.42 |
Variables con correlación negativa más fuerte (asociadas a menor probabilidad de enfermedad)#
Variable |
Correlación con HeartDisease |
|---|---|
ST_Slope_Up |
-0.62 |
ExerciseAngina_N |
-0.50 |
MaxHR |
-0.40 |
ChestPainType_ATA |
-0.40 |
Multicolinealidad#
A continuación hallamos el VIF el cual determina la fuerza de la correlación entre las variables independientes. Se pronostica tomando una variable y comparándola con todas las demás. La puntuación VIF de una variable independiente representa hasta qué punto la variable se explica por otras variables independientes.
Un valor
VIF de 1: Sin multicolinealidad (variable perfectamente independiente).Un valor
VIF entre 1 y 5: Multicolinealidad baja a moderada (no se considera problemática).Un valor
VIF entre 5 y 10: Multicolinealidad moderada a alta (considerada problemática).Un valor
VIF superior a 10: Multicolinealidad alta (preocupación grave, requiere medidas).
binarias_num.remove("HeartDisease")
encoder_vif = OneHotEncoder(handle_unknown="ignore", drop="first")
encoded_vif = encoder_vif.fit_transform(df[categoricas + binarias_cat])
df_vif = pd.DataFrame(
encoded_vif.toarray(),
columns=encoder_vif.get_feature_names_out(categoricas + binarias_cat),
index=df.index
)
df_vif = pd.concat([df[numericas + binarias_num], df_vif], axis=1)
def VIF_calculation(X):
VIF = pd.DataFrame()
VIF["variable"] = X.columns
VIF["VIF"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
VIF = VIF.sort_values('VIF', ascending=False).reset_index(drop = True)
return(VIF)
VIF_mat = VIF_calculation(df_vif)
display(VIF_mat)
| variable | VIF | |
|---|---|---|
| 0 | RestingBP | 56.433007 |
| 1 | Age | 34.368435 |
| 2 | Cholesterol | 30.640583 |
| 3 | MaxHR | 28.926409 |
| 4 | ST_Slope_Up | 9.181769 |
| 5 | ST_Slope_Flat | 8.385790 |
| 6 | Sex_M | 4.899168 |
| 7 | RestingECG_Normal | 3.840670 |
| 8 | Oldpeak | 2.703413 |
| 9 | ExerciseAngina_Y | 2.657791 |
| 10 | RestingECG_ST | 1.977057 |
| 11 | ChestPainType_ATA | 1.839491 |
| 12 | ChestPainType_NAP | 1.627091 |
| 13 | FastingBS | 1.427475 |
| 14 | ChestPainType_TA | 1.183111 |
Variables con VIF Críticamente Alto (>10):
RestingBP(VIF: 56.43) → Multicolinealidad extremaAge(VIF: 34.37) → Multicolinealidad muy altaCholesterol(VIF: 30.64) → Multicolinealidad muy altaMaxHR(VIF: 28.93) → Multicolinealidad muy alta
Interpretación de las Relaciones Problemáticas:
RestingBPcon VIF 56.43: Está explicada en un 98%+ por las otras variables.Agecon VIF 34.37: La edad está altamente correlacionada con las otras variables fisiológicas.Relación esperada: Pacientes mayores → Mayor presión arterial + Mayor colesterol + Menor frecuencia cardíaca máxima.
Tratamiento de Multicolinealidad#
Procedemos a tratar la multicolinealidad mediante la eliminación iterativa de variables con Factor de Inflación de la Varianza (VIF) superior a 10. Este enfoque sistemático nos permite identificar y remover las variables que presentan alta correlación con otras variables predictoras, lo que podría distorsionar los coeficientes del modelo y afectar su interpretabilidad. El proceso se repite de forma iterativa hasta que todas las variables restantes muestran un VIF por debajo del umbral establecido, garantizando así la independencia entre los predictores y mejorando la estabilidad de las estimaciones del modelo.
X_vif = df_vif.copy()
removed_features = []
threshold = 10
print("Proceso iterativo de eliminación por VIF:")
print("-----------------------------------------")
for i in range(df_vif.shape[1]):
vif_df = VIF_calculation(X_vif)
max_vif = vif_df['VIF'].iloc[0]
max_feature = vif_df['variable'].iloc[0]
if max_vif > threshold:
print(f"Round {i+1}: Eliminando '{max_feature}' con VIF = {max_vif:.2f}")
X_vif = X_vif.drop(columns=[max_feature])
removed_features.append(max_feature)
else:
print(f"\n¡Proceso completado! Todas las variables tienen VIF < {threshold}.")
break
Proceso iterativo de eliminación por VIF:
-----------------------------------------
Round 1: Eliminando 'RestingBP' con VIF = 56.43
Round 2: Eliminando 'Cholesterol' con VIF = 28.82
Round 3: Eliminando 'MaxHR' con VIF = 21.48
Round 4: Eliminando 'Age' con VIF = 16.94
¡Proceso completado! Todas las variables tienen VIF < 10.
Creación del Conjunto de Entrenamiento y Prueba#
Para evaluar de manera rigurosa y justa el desempeño final del modelo optimizado, es esencial probarlo en datos que no hayan sido vistos durante el proceso de ajuste de hiperparámetro. Por esta razón, se divide el dataset en subconjuntos de entrenamiento y prueba. El conjunto de entrenamiento (X_train, y_train) se utilizará para reentrenar el mejor modelo con todos los datos disponibles de entrenamiento, mientras que el conjunto de prueba (X_test, y_test) se reservará exclusivamente para la evaluación final, proporcionando una estimación no sesgada del rendimiento del modelo en datos nuevos.
Se divide las features (X) y la variable objetivo binaria (y), utilizando el parámetro stratify para garantizar que la proporción de las clases HeartDisease se mantenga igual en ambos conjuntos, preservando así el balance original.
Un 20% de los datos se asigna para prueba en ambos casos, utilizando una semilla (random_state=42) para asegurar la reproducibilidad de la partición.
y = df["HeartDisease"]
X = df.drop(columns=["HeartDisease", "RestingBP", "Cholesterol", "MaxHR", "Age"])
numericas = [col for col in numericas if col not in ["Cholesterol", "RestingBP", "MaxHR", "Age"]]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
Construcción del Preprocesador Central con ColumnTransformer#
Para optimizar el desempeño de cada algoritmo, se implementaron estrategias de preprocesamiento diferenciadas que cumplen con los supuestos teóricos de cada clasificador:
Para K-Nearest Neighbors (KNN), GaussianNB y Regresión Logística: Se aplicó estandarización (
StandardScaler) a las variables numéricas para satisfacer el supuesto de normalidad, facilitar la convergencia y garantizar que las distancias sean comparables, junto con codificación one-hot para todas las variables categóricas y binarias, eliminando la primera categoría para evitar multicolinealidad.Para BernoulliNB: Dado que este modelo opera con distribuciones Bernoulli, se transformaron las variables numéricas mediante binarización (
Binarizer) con umbral 0, convirtiendo los valores en variables binarias (0/1), junto con one-hot encoding para las variables categóricas y binarias.Para MultinomialNB: Considerando que este modelo maneja distribuciones multinomiales, se discretizaron las variables numéricas en 5 bins por cuantiles (
KBinsDiscretizer) con codificación ordinal, preservando el orden natural de los datos, junto con one-hot encoding para las variables categóricas y binarias.Para Árboles de Decisión, Random Forest y XGBoost: Dado que estos algoritmos basados en árboles son invariantes a escalas y transformaciones, se mantuvieron las variables numéricas en su formato original (
passthrough), aplicando únicamente one-hot encoding a las variables categóricas y binarias para garantizar una representación adecuada.
Todos los preprocesadores fueron configurados con handle_unknown='ignore' para garantizar robustez ante categorías no vistas durante el entrenamiento, asegurando la integridad del pipeline en producción.
preprocessor_logreg = ColumnTransformer(
transformers=[
('num', StandardScaler(), numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_knn = ColumnTransformer(
transformers=[
('num', StandardScaler(), numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_gnb = ColumnTransformer(
transformers=[
('num', StandardScaler(), numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_bnb = ColumnTransformer(
transformers=[
('num', Binarizer(threshold=0.0), numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_mnb = ColumnTransformer(
transformers=[
('num', KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile'), numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_tree = ColumnTransformer(
transformers=[
('num', 'passthrough', numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_random_forest = ColumnTransformer(
transformers=[
('num', 'passthrough', numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
preprocessor_xgb = ColumnTransformer(
transformers=[
('num', 'passthrough', numericas),
('cat', OneHotEncoder(drop="first", handle_unknown='ignore'), categoricas),
('bin_cat', OneHotEncoder(drop="first", handle_unknown='ignore'), binarias_cat),
("bin_num", "passthrough", binarias_num)]
)
Construcción de Pipelines y Optimización de Hiperparámetros#
Para cumplir con el objetivo de integrar un flujo completo de machine learning, se construyeron pipelines independientes para cada algoritmo. Un pipeline encapsula de manera secuencial y automatizada todas las etapas del proceso de modelado, desde el preprocesamiento de los datos hasta la ejecución del algoritmo de aprendizaje. Esto garantiza que cada transformación se aplique de manera consistente durante el entrenamiento y la predicción, eliminando el riesgo de data leakage y asegurando la reproducibilidad de los resultados.
Aunque algunos preprocesadores son idénticos entre modelos, se mantuvieron estructuras independientes para cada pipeline con el fin de mantener un orden sistemático y facilitar el seguimiento del desarrollo. Cada pipeline y su respectivo preprocesador están organizados según el orden de los modelos estudiados en clase, comenzando con Regresión Logística y culminando con XGBoost.
Cada pipeline se compone de dos etapas fundamentales:
preprocessing: La transformación de características definida previamente en el ColumnTransformer, que aplica las estrategias de preprocesamiento específicas para cada algoritmo.classifier: El estimador de machine learning que aprenderá a predecir sobre las características transformadas.
Estrategias de Optimización Implementadas:
Para Regresión Logística: Se utilizó
LogisticRegressionCVcon validación cruzada incorporada para optimizar el parámetro de regularizaciónCsobre un rango de valores [0.01, 0.1, 1, 10, 100].Para K-Nearest Neighbors: Se configuró
GridSearchCVpara optimizar el número de vecinos, la función de peso y la métrica de distancia.Para Naive Bayes (Gaussian, Bernoulli, Multinomial): Se implementó búsqueda exhaustiva de hiperparámetros para el parámetro de suavizado
alphaen BernoulliNB y MultinomialNB.Para Árboles de Decisión y Random Forest: Se optimizaron parámetros de profundidad, división de nodos y criterios de impureza.
Para XGBoost: Se ajustaron hiperparámetros críticos como número de estimadores, profundidad máxima, tasa de aprendizaje y submuestreo.
Todos los procesos de búsqueda utilizaron validación cruzada de 5 folds y la métrica AUC-ROC como criterio de evaluación, asegurando la robustez en la selección de los mejores hiperparámetros para cada modelo.
C = [0.01, 0.1, 1, 10, 100]
pipeline_logreg = Pipeline([
("preprocessing", preprocessor_logreg),
("classifier", LogisticRegressionCV(Cs=C,
cv=5,
penalty="l2",
solver="liblinear",
max_iter=1000,
n_jobs=-1,
scoring="roc_auc"))
])
pipeline_knn = Pipeline(steps=[
("preprocessing", preprocessor_knn),
("classifier", KNeighborsClassifier())
])
param_grid_knn = {
"classifier__n_neighbors": [3, 5, 7, 9],
"classifier__weights": ["uniform", "distance"],
"classifier__metric": ["euclidean", "manhattan", "minkowski"]
}
grid_knn = GridSearchCV(
pipeline_knn,
param_grid=param_grid_knn,
cv=5,
scoring="roc_auc",
n_jobs=-1
)
pipeline_gnb = Pipeline(steps=[
("preprocessing", preprocessor_gnb),
("classifier", GaussianNB())
])
pipeline_bnb = Pipeline(steps=[
("preprocessing", preprocessor_bnb),
("classifier", BernoulliNB())
])
param_grid_bnb = {"classifier__alpha": [0.01, 0.1, 0.5, 1, 5, 10]}
grid_bnb = GridSearchCV(pipeline_bnb, param_grid=param_grid_bnb,
cv=5, scoring="roc_auc", n_jobs=-1)
pipeline_mnb = Pipeline(steps=[
("preprocessing", preprocessor_mnb),
("classifier", MultinomialNB())
])
param_grid_mnb = {"classifier__alpha": [0.01, 0.1, 0.5, 1, 5, 10]}
grid_mnb = GridSearchCV(pipeline_mnb, param_grid=param_grid_mnb,
cv=5, scoring="roc_auc", n_jobs=-1)
pipeline_tree = Pipeline(steps=[
("preprocessing", preprocessor_tree),
("classifier", DecisionTreeClassifier(random_state=42))
])
param_grid_tree = {
"classifier__max_depth": [None, 5, 10, 20],
"classifier__min_samples_split": [2, 5, 10],
"classifier__min_samples_leaf": [1, 2, 5],
"classifier__criterion": ["gini", "entropy"]
}
grid_tree = GridSearchCV(
pipeline_tree,
param_grid=param_grid_tree,
cv=5,
scoring="roc_auc",
n_jobs=-1
)
pipeline_random_forest= Pipeline(steps=[
("preprocessing", preprocessor_random_forest),
("classifier", RandomForestClassifier(random_state=42))
])
param_grid_random_forest = {
"classifier__n_estimators": [100, 200, 500],
"classifier__max_depth": [None, 10, 20],
"classifier__min_samples_split": [2, 5, 10],
"classifier__min_samples_leaf": [1, 2, 5],
"classifier__max_features": ["sqrt", "log2"]
}
grid_random_forest = GridSearchCV(
pipeline_random_forest,
param_grid=param_grid_random_forest,
cv=5,
scoring="roc_auc",
n_jobs=-1
)
pipeline_xgb = Pipeline(steps=[
("preprocessing", preprocessor_xgb),
("classifier", XGBClassifier(use_label_encoder=False, eval_metric="logloss", random_state=42))
])
param_grid_xgb = {
"classifier__n_estimators": [100, 200, 500],
"classifier__max_depth": [3, 5, 7],
"classifier__learning_rate": [0.01, 0.1, 0.2],
"classifier__subsample": [0.8, 1.0],
"classifier__colsample_bytree": [0.8, 1.0]
}
grid_xgb = GridSearchCV(
pipeline_xgb,
param_grid=param_grid_xgb,
cv=5,
scoring="roc_auc",
n_jobs=-1
)
Evaluación Comparativa de Modelos mediante Validación Cruzada#
Con el objetivo de evaluar de manera rigurosa y comparativa el desempeño predictivo de todos los algoritmos implementados, se ejecutó un proceso de validación cruzada de 5 folds sobre el conjunto de entrenamiento. Esta metodología permite obtener una estimación robusta del rendimiento de cada modelo, minimizando el sobreajuste y proporcionando una medida confiable de la capacidad de generalización. La métrica seleccionada para la comparación fue el área bajo la curva ROC (AUC-ROC), que evalúa la capacidad del modelo para distinguir entre las clases positiva y positiva a través de todos los umbrales de clasificación posibles. Los resultados se organizaron en un ranking descendente para identificar los algoritmos más prometedores que procederán a la etapa de evaluación final.
models = {
"Logistic Regression": pipeline_logreg,
"KNN": grid_knn,
"GaussianNB": pipeline_gnb,
"BernoulliNB": grid_bnb,
"MultinomialNB": grid_mnb,
"Decision Tree": grid_tree,
"Random Forest": grid_random_forest,
"XGBoost": grid_xgb
}
results = []
for name, model in models.items():
scores = cross_val_score(model, X_train, y_train, cv=5, scoring="roc_auc", n_jobs=-1)
results.append({
"Modelo": name,
"AUC Medio": scores.mean(),
"AUC Std": scores.std()
})
results_df = pd.DataFrame(results).sort_values(by="AUC Medio", ascending=False).reset_index(drop=True)
print("Comparación de Modelos (AUC en Validación Cruzada):")
display(results_df)
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[32], line 15
12 results = []
14 for name, model in models.items():
---> 15 scores = cross_val_score(model, X_train, y_train, cv=5, scoring="roc_auc", n_jobs=-1)
16 results.append({
17 "Modelo": name,
18 "AUC Medio": scores.mean(),
19 "AUC Std": scores.std()
20 })
22 results_df = pd.DataFrame(results).sort_values(by="AUC Medio", ascending=False).reset_index(drop=True)
File ~\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\_param_validation.py:216, in validate_params.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
210 try:
211 with config_context(
212 skip_parameter_validation=(
213 prefer_skip_nested_validation or global_skip_validation
214 )
215 ):
--> 216 return func(*args, **kwargs)
217 except InvalidParameterError as e:
218 # When the function is just a wrapper around an estimator, we allow
219 # the function to delegate validation to the estimator, but we replace
220 # the name of the estimator by the name of the function in the error
221 # message to avoid confusion.
222 msg = re.sub(
223 r"parameter of \w+ must be",
224 f"parameter of {func.__qualname__} must be",
225 str(e),
226 )
File ~\miniconda3\envs\ml_venv\lib\site-packages\sklearn\model_selection\_validation.py:684, in cross_val_score(estimator, X, y, groups, scoring, cv, n_jobs, verbose, params, pre_dispatch, error_score)
681 # To ensure multimetric format is not supported
682 scorer = check_scoring(estimator, scoring=scoring)
--> 684 cv_results = cross_validate(
685 estimator=estimator,
686 X=X,
687 y=y,
688 groups=groups,
689 scoring={"score": scorer},
690 cv=cv,
691 n_jobs=n_jobs,
692 verbose=verbose,
693 params=params,
694 pre_dispatch=pre_dispatch,
695 error_score=error_score,
696 )
697 return cv_results["test_score"]
File ~\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\_param_validation.py:216, in validate_params.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
210 try:
211 with config_context(
212 skip_parameter_validation=(
213 prefer_skip_nested_validation or global_skip_validation
214 )
215 ):
--> 216 return func(*args, **kwargs)
217 except InvalidParameterError as e:
218 # When the function is just a wrapper around an estimator, we allow
219 # the function to delegate validation to the estimator, but we replace
220 # the name of the estimator by the name of the function in the error
221 # message to avoid confusion.
222 msg = re.sub(
223 r"parameter of \w+ must be",
224 f"parameter of {func.__qualname__} must be",
225 str(e),
226 )
File ~\miniconda3\envs\ml_venv\lib\site-packages\sklearn\model_selection\_validation.py:411, in cross_validate(estimator, X, y, groups, scoring, cv, n_jobs, verbose, params, pre_dispatch, return_train_score, return_estimator, return_indices, error_score)
408 # We clone the estimator to make sure that all the folds are
409 # independent, and that it is pickle-able.
410 parallel = Parallel(n_jobs=n_jobs, verbose=verbose, pre_dispatch=pre_dispatch)
--> 411 results = parallel(
412 delayed(_fit_and_score)(
413 clone(estimator),
414 X,
415 y,
416 scorer=scorers,
417 train=train,
418 test=test,
419 verbose=verbose,
420 parameters=None,
421 fit_params=routed_params.estimator.fit,
422 score_params=routed_params.scorer.score,
423 return_train_score=return_train_score,
424 return_times=True,
425 return_estimator=return_estimator,
426 error_score=error_score,
427 )
428 for train, test in indices
429 )
431 _warn_or_raise_about_fit_failures(results, error_score)
433 # For callable scoring, the return type is only know after calling. If the
434 # return type is a dictionary, the error scores can now be inserted with
435 # the correct key.
File ~\miniconda3\envs\ml_venv\lib\site-packages\sklearn\utils\parallel.py:77, in Parallel.__call__(self, iterable)
72 config = get_config()
73 iterable_with_config = (
74 (_with_config(delayed_func, config), args, kwargs)
75 for delayed_func, args, kwargs in iterable
76 )
---> 77 return super().__call__(iterable_with_config)
File ~\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:2072, in Parallel.__call__(self, iterable)
2066 # The first item from the output is blank, but it makes the interpreter
2067 # progress until it enters the Try/Except block of the generator and
2068 # reaches the first `yield` statement. This starts the asynchronous
2069 # dispatch of the tasks to the workers.
2070 next(output)
-> 2072 return output if self.return_generator else list(output)
File ~\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:1682, in Parallel._get_outputs(self, iterator, pre_dispatch)
1679 yield
1681 with self._backend.retrieval_context():
-> 1682 yield from self._retrieve()
1684 except GeneratorExit:
1685 # The generator has been garbage collected before being fully
1686 # consumed. This aborts the remaining tasks if possible and warn
1687 # the user if necessary.
1688 self._exception = True
File ~\miniconda3\envs\ml_venv\lib\site-packages\joblib\parallel.py:1800, in Parallel._retrieve(self)
1789 if self.return_ordered:
1790 # Case ordered: wait for completion (or error) of the next job
1791 # that have been dispatched and not retrieved yet. If no job
(...)
1795 # control only have to be done on the amount of time the next
1796 # dispatched job is pending.
1797 if (nb_jobs == 0) or (
1798 self._jobs[0].get_status(timeout=self.timeout) == TASK_PENDING
1799 ):
-> 1800 time.sleep(0.01)
1801 continue
1803 elif nb_jobs == 0:
1804 # Case unordered: jobs are added to the list of jobs to
1805 # retrieve `self._jobs` only once completed or in error, which
(...)
1811 # timeouts before any other dispatched job has completed and
1812 # been added to `self._jobs` to be retrieved.
KeyboardInterrupt:
Modelos de Alto Rendimiento (AUC > 0.91):
XGBoost (0.925): Máximo desempeño con la mayor AUC y desviación estándar controlada, demostrando la superioridad de los algoritmos de boosting.
Random Forest (0.917): Excelente rendimiento, confirmando la efectividad de los métodos de ensamblaje.
Logistic Regression (0.915): Sorprendente desempeño para un modelo lineal, superando expectativas.
GaussianNB (0.912): Resultado destacado para un modelo simple, compitiendo con algoritmos más complejos.
Modelos de Rendimiento Medio (AUC 0.89-0.91):
MultinomialNB (0.906): Buen desempeño considerando sus limitaciones con datos continuos.
KNN (0.904): Rendimiento aceptable pero inferior a modelos más modernos.
BernoulliNB (0.89): Límite inferior del rango competitivo.
Modelo de Menor Rendimiento:
Decision Tree (0.877): Peor desempeño, evidenciando los problemas de sobreajuste típicos de árboles individuales.
Estabilidad de los Modelos (Desviación Estándar):
BernoulliNB (0.043) muestra la mayor variabilidad, indicando inconsistencia entre folds.
Logistic Regression (0.040) y GaussianNB (0.039) presentan variabilidad moderada.
XGBoost (0.033) y KNN (0.033) demuestran mayor estabilidad en sus predicciones.
Brechas de Desempeño:
Diferencia significativa entre XGBoost (1°) y Decision Tree (8°): aproximadamente 0.046 puntos de AUC.
Competitividad estrecha entre los 4 mejores modelos: diferencia de solo aproximadamente 0.008 puntos.
XGBoost se consolida como claro líder.
Hallazgos Destacados:
Los métodos de ensamblaje dominan el ranking (XGBoost, Random Forest).
Modelos lineales y probabilísticos muestran rendimiento sorprendentemente competitivo.
Árbol individual confirma su limitación frente a técnicas más avanzadas.
Todos los modelos superan el umbral de 0.87, indicando capacidad predictiva aceptable.
Entrenamiento Final y Optimización de Hiperparámetros#
Una vez identificados los modelos más prometedores mediante validación cruzada, se procedió al entrenamiento final de todos los algoritmos. Para los modelos que requieren optimización de hiperparámetros (KNN, BernoulliNB, MultinomialNB, Decision Tree, Random Forest y XGBoost) se ejecutó el proceso de búsqueda en grid completo sobre el conjunto de entrenamiento. Los modelos GaussianNB y Logistic Regression, que no requieren esta optimización o la incorporan internamente, fueron entrenados directamente con sus configuraciones predefinidas.
grid_knn.fit(X_train, y_train)
grid_bnb.fit(X_train, y_train)
grid_mnb.fit(X_train, y_train)
grid_tree.fit(X_train, y_train)
grid_random_forest.fit(X_train, y_train)
grid_xgb.fit(X_train, y_train)
pipeline_gnb.fit(X_train, y_train);
pipeline_logreg.fit(X_train, y_train);
Análisis de los Mejores Hiperparámetros Identificados#
Tras completar el proceso de búsqueda en grid, se extrajeron y evaluaron las configuraciones óptimas de hiperparámetros para cada modelo. Este análisis permite comprender las combinaciones que maximizan el rendimiento predictivo según la métrica AUC-ROC en validación cruzada, proporcionando información valiosa sobre el comportamiento de cada algoritmo y su configuración ideal para el problema de predicción de enfermedad cardíaca.
grids = {
"KNN": grid_knn,
"BernoulliNB": grid_bnb,
"MultinomialNB": grid_mnb,
"Decision Tree": grid_tree,
"Random Forest": grid_random_forest,
"XGBoost": grid_xgb
}
print("Mejores hiperparámetros encontrados:\n")
for name, grid in grids.items():
print(f"{name}:")
print(grid.best_params_)
print(f"Mejor AUC en CV: {grid.best_score_:.6f}\n")
Mejores hiperparámetros encontrados:
KNN:
{'classifier__metric': 'manhattan', 'classifier__n_neighbors': 9, 'classifier__weights': 'uniform'}
Mejor AUC en CV: 0.905056
BernoulliNB:
{'classifier__alpha': 0.01}
Mejor AUC en CV: 0.899666
MultinomialNB:
{'classifier__alpha': 0.1}
Mejor AUC en CV: 0.906733
Decision Tree:
{'classifier__criterion': 'gini', 'classifier__max_depth': 5, 'classifier__min_samples_leaf': 5, 'classifier__min_samples_split': 2}
Mejor AUC en CV: 0.889391
Random Forest:
{'classifier__max_depth': 10, 'classifier__max_features': 'sqrt', 'classifier__min_samples_leaf': 1, 'classifier__min_samples_split': 10, 'classifier__n_estimators': 100}
Mejor AUC en CV: 0.920736
XGBoost:
{'classifier__colsample_bytree': 0.8, 'classifier__learning_rate': 0.1, 'classifier__max_depth': 3, 'classifier__n_estimators': 100, 'classifier__subsample': 0.8}
Mejor AUC en CV: 0.927998
K-Nearest Neighbors (KNN)
Configuración óptima:
n_neighbors=9,metric='manhattan',weights='uniform'.Interpretación: El modelo prefiere considerar 9 vecinos más cercanos con distancia Manhattan y sin ponderación por distancia, sugiriendo que patrones locales más amplios son importantes y que la escala de características fue bien manejada por el preprocesamiento.
Naive Bayes
BernoulliNB & MultinomialNB:
alpha=0.01para ambos.Interpretación: Ambos modelos requieren mínimo suavizado (Laplace), indicando que las probabilidades estimadas desde los datos de entrenamiento son confiables y no necesitan mayor regularización. MultinomialNB supera ligeramente a BernoulliNB (0.906 vs 0.899).
Decision Tree
Configuración óptima:
max_depth=5,min_samples_split=2,min_samples_leaf=5,criterion='gini'.Interpretación: Árbol poco profundo y conservador (profundidad 5), con requisito de al menos 5 muestras por hoja. Esto revela la tendencia del árbol a sobreajustarse si no se restringe severamente..
Random Forest
Configuración óptima:
n_estimators=100,max_depth=None,min_samples_split=10,min_samples_leaf=2,max_features='sqrt'.Interpretación: Bosque con árboles profundos sin restricción pero con división conservadora (10 muestras mínimas para dividir). El uso de
sqrtpara características maximiza la diversidad entre árboles. Excelente AUC de 0.920.
XGBoost
Configuración óptima:
n_estimators=100,max_depth=3,learning_rate=0.1,subsample=0.8,colsample_bytree=0.8.Interpretación: Configuración conservadora y regularizada: árboles con profundidad 3, submuestreo del 80% en datos y características, con tasa de aprendizaje moderada. Esto previene sobreajuste mientras construye un modelo ensemble robusto.
Patrones y Hallazgos Clave
Regularización Conservadora: La mayoría de modelos prefieren configuraciones que previenen sobreajuste (alto suavizado).
Ensembles Dominantes: Random Forest y XGBoost logran los mejores scores (0.920 y superior), confirmando la ventaja de métodos de ensamblaje.
Simplicidad Efectiva: Configuraciones relativamente simples (100 estimadores, profundidades moderadas) generan excelentes resultados.
Robustez en Preprocesamiento: El buen desempeño de KNN con métrica Manhattan sugiere que el escalado de características fue efectivo.
Evaluación Final de Modelos en el Conjunto de Prueba#
Para completar el análisis de clasificación binaria, se evaluó el desempeño de todos los modelos optimizados en el conjunto de prueba reservado para esta tarea. La evaluación de modelos de clasificación requiere un análisis multidimensional que considere no solo la capacidad predictiva general (accuracy) sino también el equilibrio entre los diferentes tipos de error (falsos positivos y falsos negativos), lo cual es crucial en el contexto médico donde los costos de cada tipo de error pueden variar significativamente.
Cada modelo se evaluó mediante un conjunto exhaustivo de métricas y visualizaciones que permiten una comparación integral:
Métricas de Evaluación:
Accuracy: Proporción general de predicciones correctas.
Precision: Capacidad del modelo de evitar falsos positivos (pacientes sanos diagnosticados como enfermos).
Recall: Capacidad del modelo de evitar falsos negativos (pacientes enfermos no detectados).
F1-score: Media armónica entre Precision y Recall, ideal para conjuntos balanceados.
Visualizaciones:
Matriz de Confusión: Muestra de forma explícita los aciertos (diagonal) y los errores (falsos positivos y falsos negativos) de cada modelo.
Curva ROC y AUC: Evalúa la capacidad del modelo para distinguir entre clases a través de todos los posibles umbrales de clasificación. Un AUC de 0.5 representa un modelo aleatorio, mientras que 1.0 representa una separación perfecta.
Esta evaluación sistemática permite identificar no solo el modelo con mejor desempeño general, sino también el más adecuado según los requisitos específicos del problema clínico.
final_models = {
"Logistic Regression": pipeline_logreg,
"KNN": grid_knn,
"GaussianNB": pipeline_gnb,
"BernoulliNB": grid_bnb,
"MultinomialNB": grid_mnb,
"Decision Tree": grid_tree,
"Random Forest": grid_random_forest,
"XGBoost": grid_xgb
}
for name, model in final_models.items():
print(f"\n--- Evaluación {name} ---")
y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1]
acc = accuracy_score(y_test, y_pred)
prec = precision_score(y_test, y_pred)
rec = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
print(f"Accuracy: {acc:.6f}")
print(f"Precision: {prec:.6f}")
print(f"Recall: {rec:.6f}")
print(f"F1-score: {f1:.6f}")
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(10, 5))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
xticklabels=["No Enfermedad Cardíaca", "Enfermedad Cardíaca"],
yticklabels=["No Enfermedad Cardíaca", "Enfermedad Cardíaca"])
plt.xlabel("Predicción")
plt.ylabel("Real")
plt.title(f"Matriz de Confusión - {name}")
plt.tight_layout()
plt.show()
fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
roc_auc = auc(fpr, tpr)
plt.figure(figsize=(10, 5))
plt.plot(fpr, tpr, label=f'AUC = {roc_auc:.4f}')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title(f"Curva ROC - {name}")
plt.legend(loc="lower right")
plt.tight_layout()
plt.show()
--- Evaluación Logistic Regression ---
Accuracy: 0.869565
Precision: 0.861111
Recall: 0.911765
F1-score: 0.885714
--- Evaluación KNN ---
Accuracy: 0.853261
Precision: 0.850467
Recall: 0.892157
F1-score: 0.870813
--- Evaluación GaussianNB ---
Accuracy: 0.891304
Precision: 0.910000
Recall: 0.892157
F1-score: 0.900990
--- Evaluación BernoulliNB ---
Accuracy: 0.831522
Precision: 0.844660
Recall: 0.852941
F1-score: 0.848780
--- Evaluación MultinomialNB ---
Accuracy: 0.858696
Precision: 0.858491
Recall: 0.892157
F1-score: 0.875000
--- Evaluación Decision Tree ---
Accuracy: 0.820652
Precision: 0.841584
Recall: 0.833333
F1-score: 0.837438
--- Evaluación Random Forest ---
Accuracy: 0.847826
Precision: 0.842593
Recall: 0.892157
F1-score: 0.866667
--- Evaluación XGBoost ---
Accuracy: 0.864130
Precision: 0.881188
Recall: 0.872549
F1-score: 0.876847
Ranking de Modelos por Accuracy
GaussianNB (0.8913) - Mejor desempeño general
Logistic Regression (0.8695)
XGBoost (0.8641)
MultinomialNB (0.8586)
KNN (0.8532)
Random Forest (0.8478)
BernoulliNB (0.8315)
Decision Tree (0.8206) - Peor desempeño
Análisis por Modelo
GaussianNB - Modelo Líder
Excelente balance: Alta precision (0.9100) y recall (0.8921)
F1-score más alto (0.9009): Indica equilibrio óptimo entre precision y recall
Resultado sorprendente: Supera a modelos más complejos como XGBoost y Random Forest.
Logistic Regression - Segundo Lugar
Recall más alto (0.9117): Mejor detectando pacientes enfermos.
Precision moderada (0.8611): Aceptable control de falsos positivos.
F1-score sólido (0.8857): Buen equilibrio general.
Modelos de Rendimiento Medio
XGBoost (0.8641): Desempeño consistente pero inferior al esperado.
KNN (0.8532): Resultado similar a XGBoost, por debajo de expectativas.
MultinomialNB (0.8586): Comparable a KNN a pesar de su simplicidad.
Random Forest (0.8478): Rendimiento inferior a su versión en validación.
Modelos de Bajo Rendimiento
BernoulliNB (0.8315): Limitado por su naturaleza binaria.
Decision Tree (0.8206): Confirma problemas de sobreajuste y alta varianza.
Análisis de Trade-offs Clínicos
Para Detección de Enfermedad (Recall Alto):
Logistic Regression (0.9117): Mejor para minimizar falsos negativos.
KNN, GaussianNB, MultinomialNB, Random Forest (0.8922): Buen balance.
Para Precisión Diagnóstica (Precision Alto):
GaussianNB (0.9100): Mejor para evitar falsos positivos.
XGBoost (0.8811): Segundo en precisión.
Equilibrio General (F1-score):
GaussianNB (0.9009)
Logistic Regression (0.8857)
XGBoost (0.8768)
MultinomialNB (0.8750)
Hallazgos Clave
Sorpresa en el Liderazgo: GaussianNB, un modelo simple, supera a ensembles complejos.
Consistencia de Modelos Lineales: Logistic Regression mantiene buen desempeño.
Sobreajuste en Ensembles: XGBoost y Random Forest no generalizan tan bien como en validación.
Elección por Objetivo:
Detección máxima: Logistic Regression (mejor recall).
Precisión máxima: GaussianNB (mejor precision).
Balance óptimo: GaussianNB (mejor F1-score).
GaussianNB emerge como el modelo más robusto y balanceado para este problema específico, ofreciendo el mejor equilibrio entre detectar enfermos y evitar diagnósticos falsos.
Resumen de Métricas de Evaluación en Conjunto de Prueba#
Modelo |
Accuracy |
Precision |
Recall |
F1-Score |
|---|---|---|---|---|
GaussianNB |
0.891304 |
0.910000 |
0.892157 |
0.900990 |
Logistic Regression |
0.869565 |
0.861111 |
0.911765 |
0.885714 |
XGBoost |
0.864130 |
0.881188 |
0.872549 |
0.876847 |
KNN |
0.853261 |
0.850467 |
0.892157 |
0.870813 |
MultinomialNB |
0.858696 |
0.858491 |
0.892157 |
0.875000 |
Random Forest |
0.847826 |
0.842593 |
0.892157 |
0.866667 |
BernoulliNB |
0.831522 |
0.844660 |
0.852941 |
0.848780 |
Decision Tree |
0.820652 |
0.841584 |
0.833333 |
0.837438 |
Análisis de Importancia de Variables en Modelos Basados en Árboles#
Para comprender qué factores clínicos son más determinantes en las predicciones de los modelos, se implementó una función especializada que visualiza las características más importantes de los algoritmos basados en árboles. Este análisis permite identificar las variables que tienen mayor poder predictivo según cada modelo, proporcionando insights valiosos sobre los patrones que los algoritmos han aprendido y validando la relevancia clínica de las características utilizadas. La función extrae y grafica las importancias calculadas internamente por cada modelo, ordenándolas de mayor a menor impacto en las decisiones de clasificación.
def plot_feature_importances(model, model_name, feature_names, top_n=10):
"""Grafica las top_n features más importantes de un modelo basado en árboles"""
importances = model.named_steps["classifier"].feature_importances_
indices = np.argsort(importances)[::-1][:top_n]
plt.figure(figsize=(10, 5))
bars = plt.barh(range(len(indices)), importances[indices], align="center")
plt.yticks(range(len(indices)), [feature_names[i] for i in indices])
plt.gca().invert_yaxis()
plt.xlabel("Importancia")
plt.title(f"Top {top_n} Features Importantes - {model_name}")
for i, bar in enumerate(bars):
plt.text(
bar.get_width() + 0.001,
bar.get_y() + bar.get_height()/2,
f"{importances[indices][i]:.2f}",
va="center"
)
plt.tight_layout()
plt.show()
feature_names_tree = grid_tree.best_estimator_.named_steps["preprocessing"].get_feature_names_out()
feature_names_random_forest = grid_random_forest.best_estimator_.named_steps["preprocessing"].get_feature_names_out()
feature_names_xgb = grid_xgb.best_estimator_.named_steps["preprocessing"].get_feature_names_out()
plot_feature_importances(grid_tree.best_estimator_, "Decision Tree", feature_names_tree)
plot_feature_importances(grid_random_forest.best_estimator_, "Random Forest", feature_names_random_forest)
plot_feature_importances(grid_xgb.best_estimator_, "XGBoost", feature_names_xgb)
Decision Tree - Features Principales
cat__ST_Slope_Up (0.68) - Variable dominante
num__Oldpeak (0.11)
bin_cat__Sex_M (0.09)
bin_num__FastingBS (0.04)
bin_cat__ExerciseAngina_Y (0.03)
Hallazgos:
Extrema dependencia de ST_Slope_Up (≈68% de importancia).
Poco uso de otras variables (Oldpeak solo 11%).
Estructura simple con pocas features relevantes.
Random Forest - Features Principales
cat__ST_Slope_Up (0.31)
num__Oldpeak (0.18)
cat__ST_Slope_Flat (0.15)
bin_cat__ExerciseAngina_Y (0.11)
bin_cat__Sex_M (0.05)
Hallazgos:
Distribución más balanceada que Decision Tree.
ST_Slope en conjunto (Up + Flat) ≈46% de importancia.
Mejor uso de múltiples variables predictivas.
Oldpeak gana importancia relativa (18%).
XGBoost - Features Principales
cat__ST_Slope_Up (0.53)
bin_cat__ExerciseAngina_Y (0.08)
cat__ST_Slope_Flat (0.08)
bin_cat__Sex_M (0.06)
Hallazgos:
ST_Slope_Up domina pero menos extremo (53% vs 68% en Decision Tree)
ExerciseAngina emerge como segundo predictor importante.
Distribución más diversificada entre features.
Features Consistentemente Importantes:
ST_Slope_Up: #1 en los tres modelos.
ExerciseAngina_Y: Importante en los tres modelos.
Sex_M: Consistentemente en top 5.
Evolución de la Importancia:
Decision Tree: Muy concentrado (1-2 features dominantes).
Random Forest: Más distribuido y balanceado.
XGBoost: Intermedio, con mejor balance que Decision Tree pero menos que Random Forest.
Los resultados validan el enfoque clínico: las variables de la prueba de esfuerzo son las más determinantes para predecir enfermedad cardíaca, seguidas por factores demográficos y metabólicos tradicionales.
Interpretación de Predicciones Individuales con LIME#
Para comprender el comportamiento de cada modelo a nivel de predicciones individuales y generar explicaciones interpretables de sus decisiones, se implementó el framework LIME (Local Interpretable Model-agnostic Explanations). Esta técnica permite analizar caso por caso cómo las diferentes variables clínicas influyen en la predicción final para un paciente específico, identificando qué características apoyan o contradicen el diagnóstico de enfermedad cardíaca. El análisis se centra en el paciente de prueba en la posición i=30, comparando las predicciones de todos los modelos contra el valor real conocido.
i = 30
final_models_lime = {
"Logistic Regression": pipeline_logreg,
"KNN": grid_knn.best_estimator_,
"GaussianNB": pipeline_gnb,
"BernoulliNB": grid_bnb.best_estimator_,
"MultinomialNB": grid_mnb.best_estimator_,
"Decision Tree": grid_tree.best_estimator_,
"Random Forest": grid_random_forest.best_estimator_,
"XGBoost": grid_xgb.best_estimator_,
}
for name, model in final_models_lime.items():
print(f"\n=== {name.upper()} ===")
preprocessing = model.named_steps["preprocessing"]
classifier = model.named_steps["classifier"]
X_train_transformed = preprocessing.transform(X_train)
X_test_transformed = preprocessing.transform(X_test)
explainer = lime.lime_tabular.LimeTabularExplainer(
training_data=X_train_transformed,
feature_names=preprocessing.get_feature_names_out(),
class_names=["No Enfermedad", "Enfermedad"],
mode="classification"
)
pred = model.predict(X_test.iloc[[i]])[0]
proba = model.predict_proba(X_test.iloc[[i]])[0][1]
print(f"Predicción: {pred} | Probabilidad de Enfermedad: {proba:.4f} | Real: {y_test.iloc[i]}")
exp = explainer.explain_instance(
data_row=X_test_transformed[i],
predict_fn=classifier.predict_proba,
num_features=10
)
exp.show_in_notebook(show_table=True)
=== LOGISTIC REGRESSION ===
Predicción: 0 | Probabilidad de Enfermedad: 0.2411 | Real: 0
=== KNN ===
Predicción: 0 | Probabilidad de Enfermedad: 0.1111 | Real: 0
=== GAUSSIANNB ===
Predicción: 0 | Probabilidad de Enfermedad: 0.1078 | Real: 0
=== BERNOULLINB ===
Predicción: 1 | Probabilidad de Enfermedad: 0.7325 | Real: 0
=== MULTINOMIALNB ===
Predicción: 1 | Probabilidad de Enfermedad: 0.6399 | Real: 0
=== DECISION TREE ===
Predicción: 0 | Probabilidad de Enfermedad: 0.2000 | Real: 0
=== RANDOM FOREST ===
Predicción: 0 | Probabilidad de Enfermedad: 0.3776 | Real: 0
=== XGBOOST ===
Predicción: 0 | Probabilidad de Enfermedad: 0.1202 | Real: 0
Contexto general
Predicción: lo que el modelo predijo para el paciente (0 = No enfermedad, 1 = Enfermedad).
Probabilidad: nivel de confianza del modelo en que tiene enfermedad.
Real: valor verdadero de la etiqueta en los datos de prueba (para este caso siempre es 0 = No enfermedad).
Las barras indican qué variables empujan la predicción hacia “No enfermedad” (azul) o hacia “Enfermedad” (naranja).
La tabla de la derecha muestra los valores reales de las características para este paciente.
Logistic Regression
Predijo No enfermedad con prob. 0.24 de enfermedad.
Factores que lo alejaron de “Enfermedad”:
ChestPainType_NAP = 1, ExerciseAngina_Y = 0, Oldpeak = -0.75.
Variables que empujaban hacia “Enfermedad”:
ST_Slope_Flat = 1, algunas categorías de ChestPainType.
Conclusión: el modelo está razonablemente alineado con la verdad (clasifica No enfermedad).
KNN
Predijo No enfermedad con prob. baja de enfermedad (0.11).
Más influencias hacia No enfermedad:
ExerciseAngina = 0, ChestPainType_NAP = 1.
Factores hacia enfermedad: ST_Slope_Flat = 1, ChestPainType_TA.
Resultado también correcto (coincide con la etiqueta real).
GaussianNB
Predijo No enfermedad con prob. 0.11.
Variables que empujan fuerte hacia enfermedad: ST_Slope_Flat, ST_Slope_Up, ChestPainType_TA.
Hacia no enfermedad: ExerciseAngina = 0, FastingBS = 0.
El modelo está bastante influenciado por las pendientes del ST (electrocardiograma).
BernoulliNB
Predijo Enfermedad (incorrecto), con prob. 0.73.
Variables claves que empujaron hacia enfermedad:
ST_Slope_Flat = 1, Oldpeak = 1, ChestPainType_NAP = 1.
Aunque el paciente real no tiene enfermedad, este modelo se dejó llevar por esos valores.
MultinomialNB
Predijo Enfermedad (incorrecto), prob. 0.64.
Empujes hacia enfermedad:
ChestPainType_TA, ST_Slope_Flat, ChestPainType_NAP.
De nuevo, se confunde porque esas combinaciones de características son muy frecuentes en pacientes con enfermedad.
Decision Tree
Predijo No enfermedad (correcto), prob. 0.20 de enfermedad.
Lo más influyente hacia enfermedad: ST_Slope_Up = 0, RestingECG_Normal = 0.
Hacia no enfermedad: FastingBS = 0, ExerciseAngina = 0.
Decisión clara y alineada con el dato real.
Random Forest
Predijo No enfermedad, prob. 0.38 de enfermedad (más alta que otros).
Variables más relevantes: ST_Slope_Up, Oldpeak, ExerciseAngina.
El bosque de árboles muestra una probabilidad mayor de error, pero aún acierta.
XGBoost
Predijo No enfermedad, prob. 0.12 de enfermedad.
Variables más influyentes: FastingBS, Oldpeak, ST_Slope_Up.
Es el modelo que más ajustado estuvo a la etiqueta real (baja probabilidad de enfermedad).
Conclusión general
Logistic Regression, KNN, GaussianNB, Decision Tree, Random Forest y XGBoost predijeron correctamente No enfermedad.
BernoulliNB y MultinomialNB se equivocaron: sobreestimaron el riesgo por ciertas variables categóricas y “Oldpeak”.
Las variables más recurrentes en la decisión fueron: ST_Slope (pendiente del ST en ECG), ChestPainType, ExerciseAngina, Oldpeak, y FastingBS.
Resumen de la Evaluación Comparativa de Modelos (Paciente de Prueba)#
Modelo |
Predicción |
Prob. Enfermedad |
Real |
¿Correcto? |
Principales variables influyentes |
|---|---|---|---|---|---|
Logistic Regression |
No enfermedad |
0.24 |
0 (No enfermedad) |
Sí |
ChestPainType_NAP (–), ExerciseAngina=0 (–), Oldpeak=–0.75 (–), ST_Slope_Flat (+) |
KNN |
No enfermedad |
0.11 |
0 |
Sí |
ExerciseAngina=0 (–), ChestPainType_NAP (–), ST_Slope_Flat (+), ChestPainType_TA (+) |
GaussianNB |
No enfermedad |
0.11 |
0 |
Sí |
ExerciseAngina=0 (–), FastingBS=0 (–), ST_Slope_Flat (+), ChestPainType_TA (+) |
BernoulliNB |
Enfermedad |
0.73 |
0 |
No |
ST_Slope_Flat (+), Oldpeak (+), ChestPainType_NAP (+) |
MultinomialNB |
Enfermedad |
0.64 |
0 |
No |
ChestPainType_TA (+), ST_Slope_Flat (+), ChestPainType_NAP (+) |
Decision Tree |
No enfermedad |
0.20 |
0 |
Sí |
FastingBS=0 (–), ExerciseAngina=0 (–), ST_Slope_Up=0 (+), RestingECG_Normal=0 (+) |
Random Forest |
No enfermedad |
0.38 |
0 |
Sí |
ST_Slope_Up (+), Oldpeak (+), ExerciseAngina (–) |
XGBoost |
No enfermedad |
0.12 |
0 |
Sí |
FastingBS (–), Oldpeak (–), ST_Slope_Up (+) |
Nota:
(+) = empuja hacia Enfermedad.
(–) = empuja hacia No enfermedad.